From e059305c86676ab946490bf2eebdf6e7ec86fb4c Mon Sep 17 00:00:00 2001 From: Philoul Date: Tue, 10 Oct 2023 18:56:26 +0200 Subject: [PATCH 01/22] Wear CWF Fix double Down dynValue --- .../src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index 345812cb27..589ccb9cf1 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -554,7 +554,7 @@ class CustomWatchface : BaseWatchFace() { FLAT("\u2192", R.drawable.ic_flat, ResFileMap.ARROW_FLAT, 4.0), FORTY_FIVE_DOWN("\u2198", R.drawable.ic_fortyfivedown, ResFileMap.ARROW_FORTY_FIVE_DOWN, 3.0), SINGLE_DOWN("\u2193", R.drawable.ic_singledown, ResFileMap.ARROW_SINGLE_DOWN, 2.0), - DOUBLE_DOWN("\u21ca", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 2.0), + DOUBLE_DOWN("\u21ca", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 1.0), TRIPLE_DOWN("X", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 1.0); companion object { From 36354aa239a31ff8b676505a2c2ac45daae030b7 Mon Sep 17 00:00:00 2001 From: Philoul Date: Wed, 11 Oct 2023 08:36:29 +0200 Subject: [PATCH 02/22] Wear CWF Add Analog G-Watch into assets --- .../src/main/assets/Analog G-Watch.zip | Bin 0 -> 76059 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 plugins/configuration/src/main/assets/Analog G-Watch.zip diff --git a/plugins/configuration/src/main/assets/Analog G-Watch.zip b/plugins/configuration/src/main/assets/Analog G-Watch.zip new file mode 100644 index 0000000000000000000000000000000000000000..304a0bd85e4cd8e2cbc586bcb79c6c416d194465 GIT binary patch literal 76059 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 literal 0 HcmV?d00001 From d242dd31296545f81996420f0b8bb6fa43fb4a2e Mon Sep 17 00:00:00 2001 From: olorinmaia Date: Wed, 11 Oct 2023 17:37:36 +0200 Subject: [PATCH 03/22] Screen forced ON while activating or deactivating Omnipod Dash --- .../ui/wizard/common/activity/OmnipodWizardActivityBase.kt | 2 ++ .../ui/wizard/deactivation/PodDeactivationWizardActivity.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/common/activity/OmnipodWizardActivityBase.kt b/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/common/activity/OmnipodWizardActivityBase.kt index 4bcd349a12..2628c2d0d3 100644 --- a/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/common/activity/OmnipodWizardActivityBase.kt +++ b/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/common/activity/OmnipodWizardActivityBase.kt @@ -1,6 +1,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.common.activity import android.os.Bundle +import android.view.WindowManager import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AlertDialog import androidx.navigation.NavController @@ -12,6 +13,7 @@ abstract class OmnipodWizardActivityBase : TranslatedDaggerAppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { exitActivityAfterConfirmation() diff --git a/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/deactivation/PodDeactivationWizardActivity.kt b/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/deactivation/PodDeactivationWizardActivity.kt index e08c40f895..de2487aa59 100644 --- a/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/deactivation/PodDeactivationWizardActivity.kt +++ b/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/deactivation/PodDeactivationWizardActivity.kt @@ -1,6 +1,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.deactivation import android.os.Bundle +import android.view.WindowManager import info.nightscout.androidaps.plugins.pump.omnipod.common.R import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.common.activity.OmnipodWizardActivityBase @@ -8,6 +9,7 @@ abstract class PodDeactivationWizardActivity : OmnipodWizardActivityBase() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) setContentView(R.layout.omnipod_common_pod_deactivation_wizard_activity) From 7942504944db220b3670e36b3d9c4591ba0ce91c Mon Sep 17 00:00:00 2001 From: olorinmaia Date: Wed, 11 Oct 2023 19:02:00 +0200 Subject: [PATCH 04/22] Screen forced ON while activating or deactivating Omnipod Dash --- .../ui/wizard/deactivation/PodDeactivationWizardActivity.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/deactivation/PodDeactivationWizardActivity.kt b/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/deactivation/PodDeactivationWizardActivity.kt index de2487aa59..e08c40f895 100644 --- a/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/deactivation/PodDeactivationWizardActivity.kt +++ b/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/deactivation/PodDeactivationWizardActivity.kt @@ -1,7 +1,6 @@ package info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.deactivation import android.os.Bundle -import android.view.WindowManager import info.nightscout.androidaps.plugins.pump.omnipod.common.R import info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.common.activity.OmnipodWizardActivityBase @@ -9,7 +8,6 @@ abstract class PodDeactivationWizardActivity : OmnipodWizardActivityBase() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) setContentView(R.layout.omnipod_common_pod_deactivation_wizard_activity) From bb133cdbce1b9c63ef8d853cebcfcc3cf1f5dad7 Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Thu, 12 Oct 2023 19:45:25 +0200 Subject: [PATCH 05/22] Fix database/impl androidTest. We need the 22.json schema file, since the test tests the migration. The junit-jupiter-api dependency causes errors during build (when the package is merged) and it's not needed for androidTest. --- core/main/test_dependencies.gradle | 1 - .../22.json | 3605 +++++++++++++++++ .../aaps/database/impl/HeartRateDaoTest.kt | 8 +- 3 files changed, 3609 insertions(+), 5 deletions(-) create mode 100644 database/impl/schemas/app.aaps.database.impl.AppDatabase/22.json diff --git a/core/main/test_dependencies.gradle b/core/main/test_dependencies.gradle index c0ca1a6673..800cb42b2e 100644 --- a/core/main/test_dependencies.gradle +++ b/core/main/test_dependencies.gradle @@ -13,7 +13,6 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.test.ext:junit-ktx:$androidx_junit_version" androidTestImplementation "androidx.test:rules:$androidx_rules_version" - androidTestImplementation "org.junit.jupiter:junit-jupiter-api:$junit_jupiter_version" androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' } diff --git a/database/impl/schemas/app.aaps.database.impl.AppDatabase/22.json b/database/impl/schemas/app.aaps.database.impl.AppDatabase/22.json new file mode 100644 index 0000000000..93af25de94 --- /dev/null +++ b/database/impl/schemas/app.aaps.database.impl.AppDatabase/22.json @@ -0,0 +1,3605 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "09121464fb795b3c37bb1c2c2c3ea481", + "entities": [ + { + "tableName": "apsResults", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `algorithm` TEXT NOT NULL, `glucoseStatusJson` TEXT NOT NULL, `currentTempJson` TEXT NOT NULL, `iobDataJson` TEXT NOT NULL, `profileJson` TEXT NOT NULL, `autosensDataJson` TEXT, `mealDataJson` TEXT NOT NULL, `isMicroBolusAllowed` INTEGER, `resultJson` TEXT NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `apsResults`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "algorithm", + "columnName": "algorithm", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "glucoseStatusJson", + "columnName": "glucoseStatusJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentTempJson", + "columnName": "currentTempJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "iobDataJson", + "columnName": "iobDataJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profileJson", + "columnName": "profileJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "autosensDataJson", + "columnName": "autosensDataJson", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mealDataJson", + "columnName": "mealDataJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isMicroBolusAllowed", + "columnName": "isMicroBolusAllowed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "resultJson", + "columnName": "resultJson", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_apsResults_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_apsResults_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_apsResults_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_apsResults_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "apsResults", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "boluses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `amount` REAL NOT NULL, `type` TEXT NOT NULL, `notes` TEXT, `isBasalInsulin` INTEGER NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, `insulinLabel` TEXT, `insulinEndTime` INTEGER, `peak` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `boluses`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isBasalInsulin", + "columnName": "isBasalInsulin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "insulinConfiguration.insulinLabel", + "columnName": "insulinLabel", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "insulinConfiguration.insulinEndTime", + "columnName": "insulinEndTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "insulinConfiguration.peak", + "columnName": "peak", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_boluses_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_boluses_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_boluses_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_boluses_isValid` ON `${TABLE_NAME}` (`isValid`)" + }, + { + "name": "index_boluses_temporaryId", + "unique": false, + "columnNames": [ + "temporaryId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_boluses_temporaryId` ON `${TABLE_NAME}` (`temporaryId`)" + }, + { + "name": "index_boluses_pumpId", + "unique": false, + "columnNames": [ + "pumpId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_boluses_pumpId` ON `${TABLE_NAME}` (`pumpId`)" + }, + { + "name": "index_boluses_pumpSerial", + "unique": false, + "columnNames": [ + "pumpSerial" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_boluses_pumpSerial` ON `${TABLE_NAME}` (`pumpSerial`)" + }, + { + "name": "index_boluses_pumpType", + "unique": false, + "columnNames": [ + "pumpType" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_boluses_pumpType` ON `${TABLE_NAME}` (`pumpType`)" + }, + { + "name": "index_boluses_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_boluses_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_boluses_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_boluses_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "boluses", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "bolusCalculatorResults", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `targetBGLow` REAL NOT NULL, `targetBGHigh` REAL NOT NULL, `isf` REAL NOT NULL, `ic` REAL NOT NULL, `bolusIOB` REAL NOT NULL, `wasBolusIOBUsed` INTEGER NOT NULL, `basalIOB` REAL NOT NULL, `wasBasalIOBUsed` INTEGER NOT NULL, `glucoseValue` REAL NOT NULL, `wasGlucoseUsed` INTEGER NOT NULL, `glucoseDifference` REAL NOT NULL, `glucoseInsulin` REAL NOT NULL, `glucoseTrend` REAL NOT NULL, `wasTrendUsed` INTEGER NOT NULL, `trendInsulin` REAL NOT NULL, `cob` REAL NOT NULL, `wasCOBUsed` INTEGER NOT NULL, `cobInsulin` REAL NOT NULL, `carbs` REAL NOT NULL, `wereCarbsUsed` INTEGER NOT NULL, `carbsInsulin` REAL NOT NULL, `otherCorrection` REAL NOT NULL, `wasSuperbolusUsed` INTEGER NOT NULL, `superbolusInsulin` REAL NOT NULL, `wasTempTargetUsed` INTEGER NOT NULL, `totalInsulin` REAL NOT NULL, `percentageCorrection` INTEGER NOT NULL, `profileName` TEXT NOT NULL, `note` TEXT NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `bolusCalculatorResults`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "targetBGLow", + "columnName": "targetBGLow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "targetBGHigh", + "columnName": "targetBGHigh", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "isf", + "columnName": "isf", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "ic", + "columnName": "ic", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "bolusIOB", + "columnName": "bolusIOB", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wasBolusIOBUsed", + "columnName": "wasBolusIOBUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "basalIOB", + "columnName": "basalIOB", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wasBasalIOBUsed", + "columnName": "wasBasalIOBUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "glucoseValue", + "columnName": "glucoseValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wasGlucoseUsed", + "columnName": "wasGlucoseUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "glucoseDifference", + "columnName": "glucoseDifference", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "glucoseInsulin", + "columnName": "glucoseInsulin", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "glucoseTrend", + "columnName": "glucoseTrend", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wasTrendUsed", + "columnName": "wasTrendUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "trendInsulin", + "columnName": "trendInsulin", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "cob", + "columnName": "cob", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wasCOBUsed", + "columnName": "wasCOBUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cobInsulin", + "columnName": "cobInsulin", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "carbs", + "columnName": "carbs", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wereCarbsUsed", + "columnName": "wereCarbsUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "carbsInsulin", + "columnName": "carbsInsulin", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "otherCorrection", + "columnName": "otherCorrection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wasSuperbolusUsed", + "columnName": "wasSuperbolusUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "superbolusInsulin", + "columnName": "superbolusInsulin", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "wasTempTargetUsed", + "columnName": "wasTempTargetUsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "totalInsulin", + "columnName": "totalInsulin", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "percentageCorrection", + "columnName": "percentageCorrection", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileName", + "columnName": "profileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_bolusCalculatorResults_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bolusCalculatorResults_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_bolusCalculatorResults_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bolusCalculatorResults_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + }, + { + "name": "index_bolusCalculatorResults_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bolusCalculatorResults_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_bolusCalculatorResults_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_bolusCalculatorResults_isValid` ON `${TABLE_NAME}` (`isValid`)" + } + ], + "foreignKeys": [ + { + "table": "bolusCalculatorResults", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "carbs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `amount` REAL NOT NULL, `notes` TEXT, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `carbs`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_carbs_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_carbs_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_carbs_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_carbs_isValid` ON `${TABLE_NAME}` (`isValid`)" + }, + { + "name": "index_carbs_nightscoutId", + "unique": false, + "columnNames": [ + "nightscoutId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_carbs_nightscoutId` ON `${TABLE_NAME}` (`nightscoutId`)" + }, + { + "name": "index_carbs_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_carbs_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_carbs_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_carbs_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "carbs", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "effectiveProfileSwitches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `basalBlocks` TEXT NOT NULL, `isfBlocks` TEXT NOT NULL, `icBlocks` TEXT NOT NULL, `targetBlocks` TEXT NOT NULL, `glucoseUnit` TEXT NOT NULL, `originalProfileName` TEXT NOT NULL, `originalCustomizedName` TEXT NOT NULL, `originalTimeshift` INTEGER NOT NULL, `originalPercentage` INTEGER NOT NULL, `originalDuration` INTEGER NOT NULL, `originalEnd` INTEGER NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, `insulinLabel` TEXT NOT NULL, `insulinEndTime` INTEGER NOT NULL, `peak` INTEGER NOT NULL, FOREIGN KEY(`referenceId`) REFERENCES `effectiveProfileSwitches`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "basalBlocks", + "columnName": "basalBlocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isfBlocks", + "columnName": "isfBlocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icBlocks", + "columnName": "icBlocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "targetBlocks", + "columnName": "targetBlocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "glucoseUnit", + "columnName": "glucoseUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalProfileName", + "columnName": "originalProfileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalCustomizedName", + "columnName": "originalCustomizedName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "originalTimeshift", + "columnName": "originalTimeshift", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originalPercentage", + "columnName": "originalPercentage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originalDuration", + "columnName": "originalDuration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "originalEnd", + "columnName": "originalEnd", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "insulinConfiguration.insulinLabel", + "columnName": "insulinLabel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "insulinConfiguration.insulinEndTime", + "columnName": "insulinEndTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insulinConfiguration.peak", + "columnName": "peak", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_effectiveProfileSwitches_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_effectiveProfileSwitches_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_effectiveProfileSwitches_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_effectiveProfileSwitches_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_effectiveProfileSwitches_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_effectiveProfileSwitches_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + }, + { + "name": "index_effectiveProfileSwitches_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_effectiveProfileSwitches_isValid` ON `${TABLE_NAME}` (`isValid`)" + } + ], + "foreignKeys": [ + { + "table": "effectiveProfileSwitches", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "extendedBoluses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `amount` REAL NOT NULL, `isEmulatingTempBasal` INTEGER NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `extendedBoluses`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "amount", + "columnName": "amount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "isEmulatingTempBasal", + "columnName": "isEmulatingTempBasal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_extendedBoluses_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_extendedBoluses_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_extendedBoluses_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_extendedBoluses_isValid` ON `${TABLE_NAME}` (`isValid`)" + }, + { + "name": "index_extendedBoluses_endId", + "unique": false, + "columnNames": [ + "endId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_extendedBoluses_endId` ON `${TABLE_NAME}` (`endId`)" + }, + { + "name": "index_extendedBoluses_pumpSerial", + "unique": false, + "columnNames": [ + "pumpSerial" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_extendedBoluses_pumpSerial` ON `${TABLE_NAME}` (`pumpSerial`)" + }, + { + "name": "index_extendedBoluses_pumpId", + "unique": false, + "columnNames": [ + "pumpId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_extendedBoluses_pumpId` ON `${TABLE_NAME}` (`pumpId`)" + }, + { + "name": "index_extendedBoluses_pumpType", + "unique": false, + "columnNames": [ + "pumpType" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_extendedBoluses_pumpType` ON `${TABLE_NAME}` (`pumpType`)" + }, + { + "name": "index_extendedBoluses_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_extendedBoluses_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_extendedBoluses_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_extendedBoluses_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "extendedBoluses", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "glucoseValues", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `raw` REAL, `value` REAL NOT NULL, `trendArrow` TEXT NOT NULL, `noise` REAL, `sourceSensor` TEXT NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `glucoseValues`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "raw", + "columnName": "raw", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "trendArrow", + "columnName": "trendArrow", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "noise", + "columnName": "noise", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "sourceSensor", + "columnName": "sourceSensor", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_glucoseValues_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_glucoseValues_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_glucoseValues_nightscoutId", + "unique": false, + "columnNames": [ + "nightscoutId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_glucoseValues_nightscoutId` ON `${TABLE_NAME}` (`nightscoutId`)" + }, + { + "name": "index_glucoseValues_sourceSensor", + "unique": false, + "columnNames": [ + "sourceSensor" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_glucoseValues_sourceSensor` ON `${TABLE_NAME}` (`sourceSensor`)" + }, + { + "name": "index_glucoseValues_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_glucoseValues_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_glucoseValues_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_glucoseValues_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "glucoseValues", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "profileSwitches", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `basalBlocks` TEXT NOT NULL, `isfBlocks` TEXT NOT NULL, `icBlocks` TEXT NOT NULL, `targetBlocks` TEXT NOT NULL, `glucoseUnit` TEXT NOT NULL, `profileName` TEXT NOT NULL, `timeshift` INTEGER NOT NULL, `percentage` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, `insulinLabel` TEXT NOT NULL, `insulinEndTime` INTEGER NOT NULL, `peak` INTEGER NOT NULL, FOREIGN KEY(`referenceId`) REFERENCES `profileSwitches`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "basalBlocks", + "columnName": "basalBlocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isfBlocks", + "columnName": "isfBlocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icBlocks", + "columnName": "icBlocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "targetBlocks", + "columnName": "targetBlocks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "glucoseUnit", + "columnName": "glucoseUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profileName", + "columnName": "profileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeshift", + "columnName": "timeshift", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "percentage", + "columnName": "percentage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "insulinConfiguration.insulinLabel", + "columnName": "insulinLabel", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "insulinConfiguration.insulinEndTime", + "columnName": "insulinEndTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insulinConfiguration.peak", + "columnName": "peak", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_profileSwitches_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_profileSwitches_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_profileSwitches_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_profileSwitches_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + }, + { + "name": "index_profileSwitches_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_profileSwitches_isValid` ON `${TABLE_NAME}` (`isValid`)" + }, + { + "name": "index_profileSwitches_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_profileSwitches_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_profileSwitches_nightscoutId", + "unique": false, + "columnNames": [ + "nightscoutId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_profileSwitches_nightscoutId` ON `${TABLE_NAME}` (`nightscoutId`)" + } + ], + "foreignKeys": [ + { + "table": "profileSwitches", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "temporaryBasals", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `type` TEXT NOT NULL, `isAbsolute` INTEGER NOT NULL, `rate` REAL NOT NULL, `duration` INTEGER NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `temporaryBasals`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isAbsolute", + "columnName": "isAbsolute", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rate", + "columnName": "rate", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_temporaryBasals_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryBasals_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_temporaryBasals_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryBasals_isValid` ON `${TABLE_NAME}` (`isValid`)" + }, + { + "name": "index_temporaryBasals_nightscoutId", + "unique": false, + "columnNames": [ + "nightscoutId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryBasals_nightscoutId` ON `${TABLE_NAME}` (`nightscoutId`)" + }, + { + "name": "index_temporaryBasals_pumpType", + "unique": false, + "columnNames": [ + "pumpType" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryBasals_pumpType` ON `${TABLE_NAME}` (`pumpType`)" + }, + { + "name": "index_temporaryBasals_endId", + "unique": false, + "columnNames": [ + "endId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryBasals_endId` ON `${TABLE_NAME}` (`endId`)" + }, + { + "name": "index_temporaryBasals_pumpSerial", + "unique": false, + "columnNames": [ + "pumpSerial" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryBasals_pumpSerial` ON `${TABLE_NAME}` (`pumpSerial`)" + }, + { + "name": "index_temporaryBasals_temporaryId", + "unique": false, + "columnNames": [ + "temporaryId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryBasals_temporaryId` ON `${TABLE_NAME}` (`temporaryId`)" + }, + { + "name": "index_temporaryBasals_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryBasals_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_temporaryBasals_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryBasals_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "temporaryBasals", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "temporaryTargets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `reason` TEXT NOT NULL, `highTarget` REAL NOT NULL, `lowTarget` REAL NOT NULL, `duration` INTEGER NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `temporaryTargets`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highTarget", + "columnName": "highTarget", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lowTarget", + "columnName": "lowTarget", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_temporaryTargets_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryTargets_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_temporaryTargets_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryTargets_isValid` ON `${TABLE_NAME}` (`isValid`)" + }, + { + "name": "index_temporaryTargets_nightscoutId", + "unique": false, + "columnNames": [ + "nightscoutId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryTargets_nightscoutId` ON `${TABLE_NAME}` (`nightscoutId`)" + }, + { + "name": "index_temporaryTargets_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryTargets_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_temporaryTargets_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_temporaryTargets_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "temporaryTargets", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "therapyEvents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `type` TEXT NOT NULL, `note` TEXT, `enteredBy` TEXT, `glucose` REAL, `glucoseType` TEXT, `glucoseUnit` TEXT NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `therapyEvents`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enteredBy", + "columnName": "enteredBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "glucose", + "columnName": "glucose", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "glucoseType", + "columnName": "glucoseType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "glucoseUnit", + "columnName": "glucoseUnit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_therapyEvents_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_therapyEvents_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_therapyEvents_type", + "unique": false, + "columnNames": [ + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_therapyEvents_type` ON `${TABLE_NAME}` (`type`)" + }, + { + "name": "index_therapyEvents_nightscoutId", + "unique": false, + "columnNames": [ + "nightscoutId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_therapyEvents_nightscoutId` ON `${TABLE_NAME}` (`nightscoutId`)" + }, + { + "name": "index_therapyEvents_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_therapyEvents_isValid` ON `${TABLE_NAME}` (`isValid`)" + }, + { + "name": "index_therapyEvents_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_therapyEvents_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_therapyEvents_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_therapyEvents_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "therapyEvents", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "totalDailyDoses", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `basalAmount` REAL NOT NULL, `bolusAmount` REAL NOT NULL, `totalAmount` REAL NOT NULL, `carbs` REAL NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `totalDailyDoses`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "basalAmount", + "columnName": "basalAmount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "bolusAmount", + "columnName": "bolusAmount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "totalAmount", + "columnName": "totalAmount", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "carbs", + "columnName": "carbs", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_totalDailyDoses_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_totalDailyDoses_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_totalDailyDoses_pumpId", + "unique": false, + "columnNames": [ + "pumpId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_totalDailyDoses_pumpId` ON `${TABLE_NAME}` (`pumpId`)" + }, + { + "name": "index_totalDailyDoses_pumpType", + "unique": false, + "columnNames": [ + "pumpType" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_totalDailyDoses_pumpType` ON `${TABLE_NAME}` (`pumpType`)" + }, + { + "name": "index_totalDailyDoses_pumpSerial", + "unique": false, + "columnNames": [ + "pumpSerial" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_totalDailyDoses_pumpSerial` ON `${TABLE_NAME}` (`pumpSerial`)" + }, + { + "name": "index_totalDailyDoses_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_totalDailyDoses_isValid` ON `${TABLE_NAME}` (`isValid`)" + }, + { + "name": "index_totalDailyDoses_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_totalDailyDoses_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_totalDailyDoses_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_totalDailyDoses_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "totalDailyDoses", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "apsResultLinks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `apsResultId` INTEGER NOT NULL, `smbId` INTEGER, `tbrId` INTEGER, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`apsResultId`) REFERENCES `apsResults`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`smbId`) REFERENCES `boluses`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`tbrId`) REFERENCES `temporaryBasals`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`referenceId`) REFERENCES `apsResultLinks`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "apsResultId", + "columnName": "apsResultId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "smbId", + "columnName": "smbId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbrId", + "columnName": "tbrId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_apsResultLinks_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_apsResultLinks_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_apsResultLinks_apsResultId", + "unique": false, + "columnNames": [ + "apsResultId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_apsResultLinks_apsResultId` ON `${TABLE_NAME}` (`apsResultId`)" + }, + { + "name": "index_apsResultLinks_smbId", + "unique": false, + "columnNames": [ + "smbId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_apsResultLinks_smbId` ON `${TABLE_NAME}` (`smbId`)" + }, + { + "name": "index_apsResultLinks_tbrId", + "unique": false, + "columnNames": [ + "tbrId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_apsResultLinks_tbrId` ON `${TABLE_NAME}` (`tbrId`)" + } + ], + "foreignKeys": [ + { + "table": "apsResults", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "apsResultId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "boluses", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "smbId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "temporaryBasals", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "tbrId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "apsResultLinks", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "multiwaveBolusLinks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `bolusId` INTEGER NOT NULL, `extendedBolusId` INTEGER NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`bolusId`) REFERENCES `boluses`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`extendedBolusId`) REFERENCES `extendedBoluses`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`referenceId`) REFERENCES `multiwaveBolusLinks`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bolusId", + "columnName": "bolusId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "extendedBolusId", + "columnName": "extendedBolusId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_multiwaveBolusLinks_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_multiwaveBolusLinks_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_multiwaveBolusLinks_bolusId", + "unique": false, + "columnNames": [ + "bolusId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_multiwaveBolusLinks_bolusId` ON `${TABLE_NAME}` (`bolusId`)" + }, + { + "name": "index_multiwaveBolusLinks_extendedBolusId", + "unique": false, + "columnNames": [ + "extendedBolusId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_multiwaveBolusLinks_extendedBolusId` ON `${TABLE_NAME}` (`extendedBolusId`)" + } + ], + "foreignKeys": [ + { + "table": "boluses", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "bolusId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "extendedBoluses", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "extendedBolusId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "multiwaveBolusLinks", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "preferenceChanges", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `key` TEXT NOT NULL, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "versionChanges", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `versionCode` INTEGER NOT NULL, `versionName` TEXT NOT NULL, `gitRemote` TEXT, `commitHash` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "versionCode", + "columnName": "versionCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "versionName", + "columnName": "versionName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gitRemote", + "columnName": "gitRemote", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "commitHash", + "columnName": "commitHash", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "userEntry", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `action` TEXT NOT NULL, `source` TEXT NOT NULL, `note` TEXT NOT NULL, `values` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "values", + "columnName": "values", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_userEntry_source", + "unique": false, + "columnNames": [ + "source" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_userEntry_source` ON `${TABLE_NAME}` (`source`)" + }, + { + "name": "index_userEntry_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_userEntry_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "foods", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `name` TEXT NOT NULL, `category` TEXT, `subCategory` TEXT, `portion` REAL NOT NULL, `carbs` INTEGER NOT NULL, `fat` INTEGER, `protein` INTEGER, `energy` INTEGER, `unit` TEXT NOT NULL, `gi` INTEGER, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `foods`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subCategory", + "columnName": "subCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "portion", + "columnName": "portion", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "carbs", + "columnName": "carbs", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fat", + "columnName": "fat", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "protein", + "columnName": "protein", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "energy", + "columnName": "energy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "unit", + "columnName": "unit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "gi", + "columnName": "gi", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_foods_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_foods_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_foods_nightscoutId", + "unique": false, + "columnNames": [ + "nightscoutId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_foods_nightscoutId` ON `${TABLE_NAME}` (`nightscoutId`)" + }, + { + "name": "index_foods_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_foods_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_foods_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_foods_isValid` ON `${TABLE_NAME}` (`isValid`)" + } + ], + "foreignKeys": [ + { + "table": "foods", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "deviceStatus", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `device` TEXT, `pump` TEXT, `enacted` TEXT, `suggested` TEXT, `iob` TEXT, `uploaderBattery` INTEGER NOT NULL, `configuration` TEXT, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "device", + "columnName": "device", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pump", + "columnName": "pump", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "enacted", + "columnName": "enacted", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "suggested", + "columnName": "suggested", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iob", + "columnName": "iob", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploaderBattery", + "columnName": "uploaderBattery", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "configuration", + "columnName": "configuration", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_deviceStatus_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_deviceStatus_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_deviceStatus_nightscoutId", + "unique": false, + "columnNames": [ + "nightscoutId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_deviceStatus_nightscoutId` ON `${TABLE_NAME}` (`nightscoutId`)" + }, + { + "name": "index_deviceStatus_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_deviceStatus_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "offlineEvents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `dateCreated` INTEGER NOT NULL, `isValid` INTEGER NOT NULL, `referenceId` INTEGER, `timestamp` INTEGER NOT NULL, `utcOffset` INTEGER NOT NULL, `reason` TEXT NOT NULL, `duration` INTEGER NOT NULL, `nightscoutSystemId` TEXT, `nightscoutId` TEXT, `pumpType` TEXT, `pumpSerial` TEXT, `temporaryId` INTEGER, `pumpId` INTEGER, `startId` INTEGER, `endId` INTEGER, FOREIGN KEY(`referenceId`) REFERENCES `offlineEvents`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateCreated", + "columnName": "dateCreated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isValid", + "columnName": "isValid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "referenceId", + "columnName": "referenceId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "utcOffset", + "columnName": "utcOffset", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutSystemId", + "columnName": "nightscoutSystemId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.nightscoutId", + "columnName": "nightscoutId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpType", + "columnName": "pumpType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpSerial", + "columnName": "pumpSerial", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.temporaryId", + "columnName": "temporaryId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.pumpId", + "columnName": "pumpId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.startId", + "columnName": "startId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "interfaceIDs_backing.endId", + "columnName": "endId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_offlineEvents_id", + "unique": false, + "columnNames": [ + "id" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_offlineEvents_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_offlineEvents_isValid", + "unique": false, + "columnNames": [ + "isValid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_offlineEvents_isValid` ON `${TABLE_NAME}` (`isValid`)" + }, + { + "name": "index_offlineEvents_nightscoutId", + "unique": false, + "columnNames": [ + "nightscoutId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_offlineEvents_nightscoutId` ON `${TABLE_NAME}` (`nightscoutId`)" + }, + { + "name": "index_offlineEvents_referenceId", + "unique": false, + "columnNames": [ + "referenceId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_offlineEvents_referenceId` ON `${TABLE_NAME}` (`referenceId`)" + }, + { + "name": "index_offlineEvents_timestamp", + "unique": false, + "columnNames": [ + "timestamp" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_offlineEvents_timestamp` ON `${TABLE_NAME}` (`timestamp`)" + } + ], + "foreignKeys": [ + { + "table": "offlineEvents", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "referenceId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '09121464fb795b3c37bb1c2c2c3ea481')" + ] + } +} \ No newline at end of file diff --git a/database/impl/src/androidTest/kotlin/app/aaps/database/impl/HeartRateDaoTest.kt b/database/impl/src/androidTest/kotlin/app/aaps/database/impl/HeartRateDaoTest.kt index 669a5dbe7f..11ef58980f 100644 --- a/database/impl/src/androidTest/kotlin/app/aaps/database/impl/HeartRateDaoTest.kt +++ b/database/impl/src/androidTest/kotlin/app/aaps/database/impl/HeartRateDaoTest.kt @@ -16,7 +16,7 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -internal class HeartRateDaoTest { +class HeartRateDaoTest { private val context = ApplicationProvider.getApplicationContext() private fun createDatabase() = @@ -86,9 +86,9 @@ internal class HeartRateDaoTest { dao.insertNewEntry(hr1) dao.insertNewEntry(hr2) - assertEquals(listOf(hr1, hr2), dao.getFromTime(timestamp)) - assertEquals(listOf(hr2), dao.getFromTime(timestamp + 1)) - assertTrue(dao.getFromTime(timestamp + 2).isEmpty()) + assertEquals(listOf(hr1, hr2), dao.getFromTime(timestamp).blockingGet()) + assertEquals(listOf(hr2), dao.getFromTime(timestamp + 1).blockingGet()) + assertTrue(dao.getFromTime(timestamp + 2).blockingGet().isEmpty()) } } From 5a17a05ee0ce2eb84f256272b099179870ec846a Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Thu, 12 Oct 2023 21:34:18 +0200 Subject: [PATCH 06/22] Local http server communication to integrate Garmin devices. --- .../aaps/activities/MyPreferenceFragment.kt | 3 + .../kotlin/app/aaps/di/PluginsListModule.kt | 7 + .../app/aaps/core/interfaces/logging/LTag.kt | 1 + .../app/aaps/database/entities/UserEntry.kt | 2 + .../InsertOrUpdateHeartRateTransaction.kt | 11 + .../UserEntryPresentationHelperImpl.kt | 1 + .../app/aaps/plugins/main/di/PluginsModule.kt | 4 +- .../general/garmin/DeltaVarEncodedList.kt | 187 +++++++++++++ .../main/general/garmin/GarminModule.kt | 10 + .../main/general/garmin/GarminPlugin.kt | 245 +++++++++++++++++ .../plugins/main/general/garmin/HttpServer.kt | 257 ++++++++++++++++++ .../plugins/main/general/garmin/LoopHub.kt | 41 +++ .../main/general/garmin/LoopHubImpl.kt | 88 ++++++ plugins/main/src/main/res/values/strings.xml | 3 + plugins/main/src/main/res/xml/pref_garmin.xml | 23 ++ .../general/garmin/DeltaVarEncodedListTest.kt | 192 +++++++++++++ .../main/general/garmin/GarminPluginTest.kt | 116 ++++++++ .../main/general/garmin/HttpServerTest.kt | 99 +++++++ .../main/general/garmin/LoopHubTest.kt | 201 ++++++++++++++ 19 files changed, 1490 insertions(+), 1 deletion(-) create mode 100644 plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedList.kt create mode 100644 plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminModule.kt create mode 100644 plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminPlugin.kt create mode 100644 plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/HttpServer.kt create mode 100644 plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHub.kt create mode 100644 plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHubImpl.kt create mode 100644 plugins/main/src/main/res/xml/pref_garmin.xml create mode 100644 plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedListTest.kt create mode 100644 plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/GarminPluginTest.kt create mode 100644 plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/HttpServerTest.kt create mode 100644 plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/LoopHubTest.kt diff --git a/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt b/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt index 530b990657..0d46f6a925 100644 --- a/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt @@ -43,6 +43,7 @@ import app.aaps.plugins.configuration.maintenance.MaintenancePlugin import app.aaps.plugins.constraints.safety.SafetyPlugin import app.aaps.plugins.insulin.InsulinOrefFreePeakPlugin import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin +import app.aaps.plugins.main.general.garmin.GarminPlugin import app.aaps.plugins.main.general.wear.WearPlugin import app.aaps.plugins.sensitivity.SensitivityAAPSPlugin import app.aaps.plugins.sensitivity.SensitivityOref1Plugin @@ -128,6 +129,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang @Inject lateinit var nsSettingStatus: NSSettingsStatus @Inject lateinit var openHumansUploaderPlugin: OpenHumansUploaderPlugin @Inject lateinit var diaconnG8Plugin: DiaconnG8Plugin + @Inject lateinit var garminPlugin: GarminPlugin override fun onAttach(context: Context) { AndroidSupportInjection.inject(this) @@ -229,6 +231,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang addPreferencesFromResource(app.aaps.plugins.configuration.R.xml.pref_datachoices, rootKey) addPreferencesFromResourceIfEnabled(maintenancePlugin, rootKey) addPreferencesFromResourceIfEnabled(openHumansUploaderPlugin, rootKey) + addPreferencesFromResourceIfEnabled(garminPlugin, rootKey) } initSummary(preferenceScreen, pluginId != -1) preprocessPreferences() diff --git a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt index 10b296c758..8430e9eab3 100644 --- a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt +++ b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt @@ -22,6 +22,7 @@ import app.aaps.plugins.insulin.InsulinOrefRapidActingPlugin import app.aaps.plugins.insulin.InsulinOrefUltraRapidActingPlugin import app.aaps.plugins.main.general.actions.ActionsPlugin import app.aaps.plugins.main.general.food.FoodPlugin +import app.aaps.plugins.main.general.garmin.GarminPlugin import app.aaps.plugins.main.general.overview.OverviewPlugin import app.aaps.plugins.main.general.persistentNotification.PersistentNotificationPlugin import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin @@ -465,6 +466,12 @@ abstract class PluginsListModule { @IntKey(610) abstract fun bindAvgSmoothingPlugin(plugin: AvgSmoothingPlugin): PluginBase + @Binds + @AllConfigs + @IntoMap + @IntKey(623) + abstract fun bindGarminPlugin(plugin: GarminPlugin): PluginBase + @Qualifier annotation class AllConfigs diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LTag.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LTag.kt index d516a7822f..69a38331b7 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LTag.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/logging/LTag.kt @@ -12,6 +12,7 @@ enum class LTag(val tag: String, val defaultValue: Boolean = true, val requiresR DATABASE("DATABASE"), DATATREATMENTS("DATATREATMENTS"), EVENTS("EVENTS", defaultValue = false, requiresRestart = true), + GARMIN("GARMIN"), GLUCOSE("GLUCOSE", defaultValue = false), HTTP("HTTP"), LOCATION("LOCATION"), diff --git a/database/entities/src/main/kotlin/app/aaps/database/entities/UserEntry.kt b/database/entities/src/main/kotlin/app/aaps/database/entities/UserEntry.kt index 98b04d65ac..d336cbfd11 100644 --- a/database/entities/src/main/kotlin/app/aaps/database/entities/UserEntry.kt +++ b/database/entities/src/main/kotlin/app/aaps/database/entities/UserEntry.kt @@ -187,7 +187,9 @@ data class UserEntry( Overview, //From OverViewPlugin Stats, //From Stat Activity Aaps, // MainApp + GarminDevice, Unknown //if necessary + , ; companion object { diff --git a/database/impl/src/main/java/app/aaps/database/impl/transactions/InsertOrUpdateHeartRateTransaction.kt b/database/impl/src/main/java/app/aaps/database/impl/transactions/InsertOrUpdateHeartRateTransaction.kt index c9c56975b6..14b58ea1d5 100644 --- a/database/impl/src/main/java/app/aaps/database/impl/transactions/InsertOrUpdateHeartRateTransaction.kt +++ b/database/impl/src/main/java/app/aaps/database/impl/transactions/InsertOrUpdateHeartRateTransaction.kt @@ -17,5 +17,16 @@ class InsertOrUpdateHeartRateTransaction(private val heartRate: HeartRate) : } } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as InsertOrUpdateHeartRateTransaction + return heartRate == other.heartRate + } + + override fun hashCode(): Int { + return heartRate.hashCode() + } + data class TransactionResult(val inserted: List, val updated: List) } diff --git a/implementation/src/main/kotlin/app/aaps/implementation/userEntry/UserEntryPresentationHelperImpl.kt b/implementation/src/main/kotlin/app/aaps/implementation/userEntry/UserEntryPresentationHelperImpl.kt index 46b4ca5f74..35c42443a3 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/userEntry/UserEntryPresentationHelperImpl.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/userEntry/UserEntryPresentationHelperImpl.kt @@ -108,6 +108,7 @@ class UserEntryPresentationHelperImpl @Inject constructor( Sources.ConfigBuilder -> app.aaps.core.ui.R.drawable.ic_cogs Sources.Overview -> app.aaps.core.ui.R.drawable.ic_home Sources.Aaps -> R.drawable.ic_aaps + Sources.GarminDevice -> app.aaps.core.ui.R.drawable.ic_generic_icon Sources.Unknown -> app.aaps.core.ui.R.drawable.ic_generic_icon } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt index 5f6a389b3b..bb02312d79 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt @@ -2,6 +2,7 @@ package app.aaps.plugins.main.di import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.smsCommunicator.SmsCommunicator +import app.aaps.plugins.main.general.garmin.GarminModule import app.aaps.plugins.main.general.persistentNotification.DummyService import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin import app.aaps.plugins.main.general.wear.WearFragment @@ -22,7 +23,8 @@ import dagger.android.ContributesAndroidInjector SkinsUiModule::class, ActionsModule::class, WearModule::class, - OverviewModule::class + OverviewModule::class, + GarminModule::class, ] ) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedList.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedList.kt new file mode 100644 index 0000000000..9934bd0215 --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedList.kt @@ -0,0 +1,187 @@ +package app.aaps.plugins.main.general.garmin + +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.IntBuffer +import java.nio.LongBuffer +import java.util.Base64 + +/** Efficient encoding for glucose/timestamp pairs. + * + * Garmin devices don't have much memory when deserializing received JSON messages. + * In particular older devices my kill our app when we send 2h of glucose values. Therefore, we + * encode the values efficiently. + * We use [var encoding](https://en.wikipedia.org/wiki/Variable-width_encoding). In order to + * keep timestamps small, we encode the difference to the previous pair and to encode negative values + * efficiently, we use [zig-zag encoding](https://en.wikipedia.org/wiki/Variable-length_quantity). + */ +class DeltaVarEncodedList { + private var lastValues: IntArray + private var data: ByteArray + private val start: Int = 0 + private var end: Int = 0 + + val byteSize: Int get() = end - start + var size: Int = 0 + private set + + /** Creates a new list of given size. + * + * @param byteSize How large the internal buffer should be. The buffer doesn't grow + * automatically, so you need to set it large enough. + * @param entrySize Size of each entry (e.g. 2 for glucose+timestamp). Delta is computed on each + * entrySize value. + */ + constructor(byteSize: Int, entrySize: Int) { + data = ByteArray(toLongBoundary(byteSize)) + lastValues = IntArray(entrySize) + } + + /** Creates a list from encoded values. + * + * @param lastValues the last values of the list. Needs to be entrySize long. + * @param byteBuffer the encoded data + */ + constructor(lastValues: IntArray, byteBuffer: ByteBuffer) { + this.lastValues = lastValues + data = ByteArray(byteBuffer.limit()) + byteBuffer.position(0) + byteBuffer.get(data) + end = data.size + val it = DeltaIterator() + while (it.next()) { + size++ + } + } + + /** Gets the encoded data. */ + fun encodedData(): List { + val byteBuffer: ByteBuffer = ByteBuffer.wrap(data) + byteBuffer.order(ByteOrder.LITTLE_ENDIAN) + byteBuffer.limit(toLongBoundary(end)) + val buffer: LongBuffer = byteBuffer.asLongBuffer() + val encodedData: MutableList = ArrayList(buffer.limit()) + while (buffer.position() < buffer.limit()) { + encodedData.add(buffer.get()) + } + return encodedData + } + + fun encodedBase64(): String { + val byteBuffer: ByteBuffer = ByteBuffer.wrap(data, start, end) + byteBuffer.order(ByteOrder.LITTLE_ENDIAN) + return String(Base64.getEncoder().encode(byteBuffer).array()) + } + + private fun addVarEncoded(value: Int) { + var remaining: Int = value + do { + // Grow data if needed (double size). + if (end == data.size) { + val newData = ByteArray(2 * end) + System.arraycopy(data, 0, newData, 0, end) + data = newData + } + if ((remaining and 0x7f.inv()) != 0) { + data[end++] = ((remaining and 0x7f) or 0x80).toByte() + } else { + data[end++] = remaining.toByte() + } + remaining = remaining ushr 7 + } while (remaining != 0) + } + + private fun addI(value: Int, idx: Int) { + val delta: Int = value - lastValues[idx] + addVarEncoded(zigzagEncode(delta)) + lastValues[idx] = value + } + + /** Adds an entry to the buffer. + * + * [values] length must be the same as entrySize provided in the constructor. */ + fun add(vararg values: Int) { + if (values.size != lastValues.size) { + throw IllegalArgumentException() + } + for (idx in values.indices) { + addI(values[idx], idx) + } + size++ + } + + fun toArray(): IntArray { + val values: IntBuffer = IntBuffer.allocate(lastValues.size * size) + val it = DeltaIterator() + while (it.next()) { + values.put(it.current()) + } + val next: IntArray = lastValues.copyOf(lastValues.size) + var nextIdx: Int = next.size - 1 + for (valueIdx in values.position() - 1 downTo 0) { + val value: Int = values.get(valueIdx) + values.put(valueIdx, next[nextIdx]) + next[nextIdx] -= value + nextIdx = (nextIdx + 1) % next.size + } + return values.array() + } + + private inner class DeltaIterator { + + private val buffer: ByteBuffer = ByteBuffer.wrap(data) + private val currentValues: IntArray = IntArray(lastValues.size) + private var more: Boolean = false + fun current(): IntArray { + return currentValues + } + + private fun readNext(): Int { + var v = 0 + var offset = 0 + var b: Int + do { + if (!buffer.hasRemaining()) { + more = false + return 0 + } + b = buffer.get().toInt() + v = v or ((b and 0x7f) shl offset) + offset += 7 + } while ((b and 0x80) != 0) + return zigzagDecode(v) + } + + operator fun next(): Boolean { + if (!buffer.hasRemaining()) return false + more = true + var i = 0 + while (i < currentValues.size && more) { + currentValues[i] = readNext() + i++ + } + return more + } + + init { + buffer.position(start) + buffer.limit(end) + buffer.order(ByteOrder.LITTLE_ENDIAN) + } + } + + companion object { + + private fun toLongBoundary(i: Int): Int { + return 8 * ((i + 7) / 8) + } + + private fun zigzagEncode(i: Int): Int { + return (i shr 31) xor (i shl 1) + } + + private fun zigzagDecode(i: Int): Int { + return (i ushr 1) xor -(i and 1) + } + } +} \ No newline at end of file diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminModule.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminModule.kt new file mode 100644 index 0000000000..255ceceadb --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminModule.kt @@ -0,0 +1,10 @@ +package app.aaps.plugins.main.general.garmin + +import dagger.Binds +import dagger.Module + +@Module +abstract class GarminModule { + @Suppress("unused") + @Binds abstract fun bindLoopHub(loopHub: LoopHubImpl): LoopHub +} \ No newline at end of file diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminPlugin.kt new file mode 100644 index 0000000000..6a87777301 --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminPlugin.kt @@ -0,0 +1,245 @@ +package app.aaps.plugins.main.general.garmin + +import androidx.annotation.VisibleForTesting +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.plugin.PluginBase +import app.aaps.core.interfaces.plugin.PluginDescription +import app.aaps.core.interfaces.plugin.PluginType +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.events.EventNewBG +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.main.R +import com.google.gson.JsonObject +import dagger.android.HasAndroidInjector +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.schedulers.Schedulers +import java.net.SocketAddress +import java.net.URI +import java.time.Clock +import java.time.Duration +import java.time.Instant +import java.util.* +import java.util.concurrent.locks.Condition +import java.util.concurrent.locks.ReentrantLock +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.concurrent.withLock +import kotlin.math.roundToInt + +/** Support communication with Garmin devices. + * + * This plugin supports sending glucose values to Garmin devices and receiving + * carbs, heart rate and pump disconnect events from the device. It communicates + * via HTTP on localhost or Garmin's native CIQ library. + */ +@Singleton +class GarminPlugin @Inject constructor( + injector: HasAndroidInjector, + aapsLogger: AAPSLogger, + resourceHelper: ResourceHelper, + private val loopHub: LoopHub, + private val rxBus: RxBus, + private val sp: SP, +) : PluginBase( + PluginDescription() + .mainType(PluginType.GENERAL) + .pluginName(R.string.garmin) + .shortName(R.string.garmin) + .description(R.string.garmin_description) + .preferencesId(R.xml.pref_garmin), + aapsLogger, resourceHelper, injector +) { + /** HTTP Server for local HTTP server communication (device app requests values) .*/ + private var server: HttpServer? = null + + private val disposable = CompositeDisposable() + + @VisibleForTesting + var clock: Clock = Clock.systemUTC() + + private val valueLock = ReentrantLock() + @VisibleForTesting + var newValue: Condition = valueLock.newCondition() + private var lastGlucoseValueTimestamp: Long? = null + private val glucoseUnitStr get() = if (loopHub.glucoseUnit == GlucoseUnit.MGDL) "mgdl" else "mmoll" + + private fun onPreferenceChange(event: EventPreferenceChange) { + aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}") + setupHttpServer() + } + + override fun onStart() { + super.onStart() + aapsLogger.info(LTag.GARMIN, "start") + disposable.add( + rxBus + .toObservable(EventPreferenceChange::class.java) + .observeOn(Schedulers.io()) + .subscribe(::onPreferenceChange) + ) + setupHttpServer() + } + + private fun setupHttpServer() { + 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) + } + } else if (server != null) { + aapsLogger.info(LTag.GARMIN, "stopping HTTP server") + server?.close() + server = null + } + } + + override fun onStop() { + disposable.clear() + aapsLogger.info(LTag.GARMIN, "Stop") + server?.close() + server = null + super.onStop() + } + + /** Receive new blood glucose events. + * + * Stores new blood glucose values in lastGlucoseValue to make sure we return + * these values immediately when values are requested by Garmin device. + * Sends a message to the Garmin devices via the ciqMessenger. */ + @VisibleForTesting + fun onNewBloodGlucose(event: EventNewBG) { + val timestamp = event.glucoseValueTimestamp ?: return + aapsLogger.info(LTag.GARMIN, "onNewBloodGlucose ${Date(timestamp)}") + valueLock.withLock { + if ((lastGlucoseValueTimestamp?: 0) >= timestamp) return + lastGlucoseValueTimestamp = timestamp + newValue.signalAll() + } + } + + /** Gets the last 2+ hours of glucose values. */ + @VisibleForTesting + fun getGlucoseValues(): List { + val from = clock.instant().minus(Duration.ofHours(2).plusMinutes(9)) + return loopHub.getGlucoseValues(from, true) + } + + /** Get the last 2+ hours of glucose values and waits in case a new value should arrive soon. */ + private fun getGlucoseValues(maxWait: Duration): List { + val glucoseFrequency = Duration.ofMinutes(5) + val glucoseValues = getGlucoseValues() + val last = glucoseValues.lastOrNull() ?: return emptyList() + val delay = Duration.ofMillis(clock.millis() - last.timestamp) + return if (!maxWait.isZero + && delay > glucoseFrequency + && delay < glucoseFrequency.plusMinutes(1)) { + valueLock.withLock { + aapsLogger.debug(LTag.GARMIN, "waiting for new glucose (delay=$delay)") + newValue.awaitNanos(maxWait.toNanos()) + } + getGlucoseValues() + } else { + glucoseValues + } + } + + private fun encodedGlucose(glucoseValues: List): String { + val encodedGlucose = DeltaVarEncodedList(glucoseValues.size * 16, 2) + for (glucose: GlucoseValue in glucoseValues) { + val timeSec: Int = (glucose.timestamp / 1000).toInt() + 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() + } + + /** 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") + receiveHeartRate(uri) + val profileName = loopHub.currentProfileName + val waitSec = getQueryParameter(uri, "wait", 0L) + val glucoseValues = getGlucoseValues(Duration.ofSeconds(waitSec)) + val jo = JsonObject() + jo.addProperty("encodedGlucose", encodedGlucose(glucoseValues)) + jo.addProperty("remainingInsulin", loopHub.insulinOnboard) + jo.addProperty("glucoseUnit", glucoseUnitStr) + loopHub.temporaryBasal.also { + if (!it.isNaN()) jo.addProperty("temporaryBasalRate", it) + } + 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") + } + } + + private fun getQueryParameter(uri: URI, name: String) = (uri.query ?: "") + .split("&") + .map { kv -> kv.split("=") } + .firstOrNull { kv -> kv.size == 2 && kv[0] == name }?.get(1) + + private fun getQueryParameter( + uri: URI, + @Suppress("SameParameterValue") name: String, + @Suppress("SameParameterValue") defaultValue: Boolean): Boolean { + return when (getQueryParameter(uri, name)?.lowercase()) { + "true" -> true + "false" -> false + else -> defaultValue + } + } + + private fun getQueryParameter( + uri: URI, name: String, + @Suppress("SameParameterValue") defaultValue: Long + ): Long { + val value = getQueryParameter(uri, name) + return try { + if (value.isNullOrEmpty()) defaultValue else value.toLong() + } catch (e: NumberFormatException) { + aapsLogger.error(LTag.GARMIN, "invalid $name value '$value'") + defaultValue + } + } + + @VisibleForTesting + fun receiveHeartRate(uri: URI) { + val avg: Int = getQueryParameter(uri, "hr", 0L).toInt() + val samplingStartSec: Long = getQueryParameter(uri, "hrStart", 0L) + val samplingEndSec: Long = getQueryParameter(uri, "hrEnd", 0L) + val device: String? = getQueryParameter(uri, "device") + receiveHeartRate( + Instant.ofEpochSecond(samplingStartSec), Instant.ofEpochSecond(samplingEndSec), + avg, device, getQueryParameter(uri, "test", false)) + } + + private fun receiveHeartRate( + samplingStart: Instant, samplingEnd: Instant, + avg: Int, device: String?, test: Boolean) { + aapsLogger.info(LTag.GARMIN, "average heart rate $avg BPM test=$test") + if (test) return + if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) { + loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device) + } else { + aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd") + } + } +} diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/HttpServer.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/HttpServer.kt new file mode 100644 index 0000000000..fa44d60597 --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/HttpServer.kt @@ -0,0 +1,257 @@ +package app.aaps.plugins.main.general.garmin + +import android.os.StrictMode +import androidx.annotation.VisibleForTesting +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import java.io.* +import java.lang.Thread.UncaughtExceptionHandler +import java.net.* +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.locks.ReentrantLock +import java.util.regex.Pattern +import kotlin.concurrent.withLock + +/** Basic HTTP server to communicate with Garmin device via localhost. */ +class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val port: Int): Closeable { + private val serverThread: Thread + private val workerExecutor: Executor = Executors.newCachedThreadPool() + private val endpoints: MutableMapCharSequence> = + ConcurrentHashMap() + private var serverSocket: ServerSocket? = null + private val readyLock = ReentrantLock() + private val readyCond = readyLock.newCondition() + + init { + serverThread = Thread { runServer() } + serverThread.name = "GarminHttpServer" + serverThread.isDaemon = true + serverThread.uncaughtExceptionHandler = UncaughtExceptionHandler { _, e -> + e.printStackTrace() + aapsLogger.error(LTag.GARMIN, "uncaught in HTTP server", e) + serverSocket?.use {} + } + serverThread.start() + } + + override fun close() { + try { + serverSocket?.close() + serverSocket = null + } catch (_: IOException) { + } + try { + serverThread.join(10_000L) + } catch (_: InterruptedException) { + } + } + + /** Wait for the server to start listing to requests. */ + fun awaitReady(wait: Duration): Boolean { + var waitNanos = wait.toNanos() + readyLock.withLock { + while (serverSocket?.isBound != true && waitNanos > 0L) { + waitNanos = readyCond.awaitNanos(waitNanos) + } + } + return serverSocket?.isBound ?: false + } + + /** Register an endpoint (path) to handle requests. */ + fun registerEndpoint(path: String, endpoint: (SocketAddress, URI, String?)->CharSequence) { + aapsLogger.info(LTag.GARMIN,"Register: '$path'") + endpoints[path] = endpoint + } + + + // @Suppress("all") + private fun respond( + @Suppress("SameParameterValue") code: Int, + body: CharSequence, + @Suppress("SameParameterValue") contentType: String, + out: OutputStream) { + respond(code, body.toString().toByteArray(Charset.forName("UTF8")), contentType, out) + } + + private fun respond(code: Int, out: OutputStream) { + respond(code, null as ByteArray?, null, out) + } + + private fun respond(code: Int, body: ByteArray?, contentType: String?, out: OutputStream) { + val header = StringBuilder() + header.append("HTTP/1.1 ").append(code).append(" OK\r\n") + if (body != null) { + appendHeader("Content-Length", "" + body.size, header) + } + if (contentType != null) { + appendHeader("Content-Type", contentType, header) + } + header.append("\r\n") + val bout = BufferedOutputStream(out) + bout.write(header.toString().toByteArray(StandardCharsets.US_ASCII)) + if (body != null) { + bout.write(body) + } + bout.flush() + } + + private fun handleRequest(s: Socket) { + val out = s.getOutputStream() + try { + val (uri, reqBody) = parseRequest(s.getInputStream()) + if ("favicon.ico" == uri.path) { + respond(HttpURLConnection.HTTP_NOT_FOUND, out) + return + } + val endpoint = endpoints[uri.path ?: ""] + if (endpoint == null) { + aapsLogger.error(LTag.GARMIN, "request path not found '" + uri.path + "'") + respond(HttpURLConnection.HTTP_NOT_FOUND, out) + } else { + try { + val body = endpoint(s.remoteSocketAddress, uri, reqBody) + respond(HttpURLConnection.HTTP_OK, body, "application/json", out) + } catch (e: Exception) { + aapsLogger.error(LTag.GARMIN, "endpoint " + uri.path + " failed", e) + respond(HttpURLConnection.HTTP_INTERNAL_ERROR, out) + } + } + } catch (e: SocketTimeoutException) { + // Client may just connect without sending anything. + aapsLogger.debug(LTag.GARMIN, "socket timeout: " + e.message) + return + } catch (e: IOException) { + aapsLogger.error(LTag.GARMIN, "Invalid request", e) + respond(HttpURLConnection.HTTP_BAD_REQUEST, out) + return + } + } + + private fun runServer() = try { + // Policy won't work in unit tests, so ignore NULL builder. + @Suppress("UNNECESSARY_SAFE_CALL") + val policy = StrictMode.ThreadPolicy.Builder()?.permitAll()?.build() + if (policy != null) StrictMode.setThreadPolicy(policy) + readyLock.withLock { + serverSocket = ServerSocket() + serverSocket!!.bind( + // Garmin will only connect to IP4 localhost. Therefore, we need to explicitly listen + // on that loopback interface and cannot use InetAddress.getLoopbackAddress(). That + // gives ::1 (IP6 localhost). + InetSocketAddress(Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)), port)) + readyCond.signalAll() } + aapsLogger.info(LTag.GARMIN,"accept connections on " + serverSocket!!.localSocketAddress) + while (true) { + val socket = serverSocket!!.accept() + aapsLogger.info(LTag.GARMIN,"accept " + socket.remoteSocketAddress) + workerExecutor.execute { + Thread.currentThread().name = "worker" + Thread.currentThread().id + try { + socket.use { s -> + s.soTimeout = 10_000 + handleRequest(s) + } + } catch (e: Exception) { + aapsLogger.error(LTag.GARMIN, "response failed", e) + } + } + } + } catch (e: IOException) { + aapsLogger.error("Server crashed", e) + } finally { + try { + serverSocket?.close() + serverSocket = null + } catch (e: IOException) { + aapsLogger.error(LTag.GARMIN, "Socked close failed", e) + } + } + + companion object { + private val REQUEST_HEADER = Pattern.compile("(GET|POST) (\\S*) HTTP/1.1") + private val HEADER_LINE = Pattern.compile("([A-Za-z-]+)\\s*:\\s*(.*)") + + private fun readLine(input: InputStream, charset: Charset): String { + val buffer = ByteArrayOutputStream(input.available()) + loop@while (true) { + when (val c = input.read()) { + '\r'.code -> {} + -1 -> break@loop + '\n'.code -> break@loop + else -> buffer.write(c) + } + } + return String(buffer.toByteArray(), charset) + } + + @VisibleForTesting + internal fun readBody(input: InputStream, length: Int): String { + var remaining = length + val buffer = ByteArrayOutputStream(input.available()) + var c: Int = -1 + while (remaining-- > 0 && (input.read().also { c = it }) != -1) { + buffer.write(c) + } + return buffer.toString("UTF8") + } + + /** Parses a requests and returns the URI and the request body. */ + @VisibleForTesting + internal fun parseRequest(input: InputStream): Pair { + val headerLine = readLine(input, Charset.forName("ASCII")) + val p = REQUEST_HEADER.matcher(headerLine) + if (!p.matches()) { + throw IOException("invalid HTTP header '$headerLine'") + } + val post = ("POST" == p.group(1)) + var uri = URI(p.group(2)) + val headers: MutableMap = HashMap() + while (true) { + val line = readLine(input, Charset.forName("ASCII")) + if (line.isEmpty()) { + break + } + val m = HEADER_LINE.matcher(line) + if (!m.matches()) { + throw IOException("invalid header line '$line'") + } + headers[m.group(1)!!] = m.group(2) + } + var body: String? + if (post) { + var contentLength = Int.MAX_VALUE + if (headers.containsKey("Content-Length")) { + contentLength = headers["Content-Length"]!!.toInt() + } + val keepAlive = ("Keep-Alive" == headers["Connection"]) + val contentType = headers["Content-Type"] + if (keepAlive && contentLength == Int.MAX_VALUE) { + throw IOException("keep-alive without content-length for $uri") + } + body = readBody(input, contentLength) + if (("application/x-www-form-urlencoded" == contentType)) { + uri = URI(uri.scheme, uri.userInfo, uri.host, uri.port, uri.path, body, null) + // uri.encodedQuery(body) + body = null + } else if ("application/json" != contentType && body.isNotBlank()) { + body = null + } + } else { + body = null + } + return Pair(uri, body?.takeUnless(String::isBlank)) + } + + private fun appendHeader(name: String, value: String, header: StringBuilder) { + header.append(name) + header.append(": ") + header.append(value) + header.append("\r\n") + } + } +} diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHub.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHub.kt new file mode 100644 index 0000000000..6397049377 --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHub.kt @@ -0,0 +1,41 @@ +package app.aaps.plugins.main.general.garmin + +import app.aaps.core.interfaces.db.GlucoseUnit +import app.aaps.core.interfaces.profile.Profile +import app.aaps.database.entities.GlucoseValue +import java.time.Instant + +/** Abstraction from all the functionality we need from the AAPS app. */ +interface LoopHub { + + /** Returns the active insulin profile. */ + val currentProfile: Profile? + + /** Returns the name of the active insulin profile. */ + val currentProfileName: String + + /** Returns the glucose unit (mg/dl or mmol/l) as selected by the user. */ + val glucoseUnit: GlucoseUnit + + /** Returns the remaining bolus insulin on board. */ + val insulinOnboard: Double + + /** Returns true if the pump is connected. */ + val isConnected: Boolean + + /** Returns true if the current profile is set of a limited amount of time. */ + val isTemporaryProfile: Boolean + + /** Returns the factor by which the basal rate is currently raised (> 1) or lowered (< 1). */ + val temporaryBasal: Double + + /** Retrieves the glucose values starting at from. */ + fun getGlucoseValues(from: Instant, ascending: Boolean): List + + /** Stores hear rate readings that a taken and averaged of the given interval. */ + fun storeHeartRate( + samplingStart: Instant, samplingEnd: Instant, + avgHeartRate: Int, + device: String? + ) +} \ No newline at end of file diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHubImpl.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHubImpl.kt new file mode 100644 index 0000000000..672b5a86c3 --- /dev/null +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHubImpl.kt @@ -0,0 +1,88 @@ +package app.aaps.plugins.main.general.garmin + +import androidx.annotation.VisibleForTesting +import app.aaps.core.interfaces.aps.Loop +import app.aaps.core.interfaces.db.GlucoseUnit +import app.aaps.core.interfaces.iob.IobCobCalculator +import app.aaps.core.interfaces.profile.Profile +import app.aaps.core.interfaces.profile.ProfileFunction +import app.aaps.database.ValueWrapper +import app.aaps.database.entities.EffectiveProfileSwitch +import app.aaps.database.entities.GlucoseValue +import app.aaps.database.entities.HeartRate +import app.aaps.database.impl.AppRepository +import app.aaps.database.impl.transactions.InsertOrUpdateHeartRateTransaction +import java.time.Clock +import java.time.Instant +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +/** + * Interface to the functionality of the looping algorithm and storage systems. + */ +class LoopHubImpl @Inject constructor( + private val iobCobCalculator: IobCobCalculator, + private val loop: Loop, + private val profileFunction: ProfileFunction, + private val repo: AppRepository, +) : LoopHub { + + @VisibleForTesting + var clock: Clock = Clock.systemUTC() + + /** Returns the active insulin profile. */ + override val currentProfile: Profile? get() = profileFunction.getProfile() + + /** Returns the name of the active insulin profile. */ + override val currentProfileName: String + get() = profileFunction.getProfileName() + + /** Returns the glucose unit (mg/dl or mmol/l) as selected by the user. */ + override val glucoseUnit: GlucoseUnit + get() = profileFunction.getProfile()?.units ?: GlucoseUnit.MGDL + + /** Returns the remaining bolus insulin on board. */ + override val insulinOnboard: Double + get() = iobCobCalculator.calculateIobFromBolus().iob + + /** Returns true if the pump is connected. */ + override val isConnected: Boolean get() = !loop.isDisconnected + + /** Returns true if the current profile is set of a limited amount of time. */ + override val isTemporaryProfile: Boolean + get() { + val resp = repo.getEffectiveProfileSwitchActiveAt(clock.millis()) + val ps: EffectiveProfileSwitch? = + (resp.blockingGet() as? ValueWrapper.Existing)?.value + return ps != null && ps.originalDuration > 0 + } + + /** Returns the factor by which the basal rate is currently raised (> 1) or lowered (< 1). */ + override val temporaryBasal: Double + get() { + val apsResult = loop.lastRun?.constraintsProcessed + return if (apsResult == null) Double.NaN else apsResult.percent / 100.0 + } + + /** Retrieves the glucose values starting at from. */ + override fun getGlucoseValues(from: Instant, ascending: Boolean): List { + return repo.compatGetBgReadingsDataFromTime(from.toEpochMilli(), ascending) + .blockingGet() + } + + /** Stores hear rate readings that a taken and averaged of the given interval. */ + override fun storeHeartRate( + samplingStart: Instant, samplingEnd: Instant, + avgHeartRate: Int, + device: String?) { + val hr = HeartRate( + timestamp = samplingStart.toEpochMilli(), + duration = samplingEnd.toEpochMilli() - samplingStart.toEpochMilli(), + dateCreated = clock.millis(), + beatsPerMinute = avgHeartRate.toDouble(), + device = device ?: "Garmin", + ) + repo.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait() + } +} \ No newline at end of file diff --git a/plugins/main/src/main/res/values/strings.xml b/plugins/main/src/main/res/values/strings.xml index 9f1b4c6556..18af69163d 100644 --- a/plugins/main/src/main/res/values/strings.xml +++ b/plugins/main/src/main/res/values/strings.xml @@ -401,5 +401,8 @@ DEFAULT RANGE target Rate: %1$.2fU/h (%2$.2f%%) \nDuration %3$d min + Garmin + Connection to Garmin device (Fenix, Edge, …) + Garmin settings diff --git a/plugins/main/src/main/res/xml/pref_garmin.xml b/plugins/main/src/main/res/xml/pref_garmin.xml new file mode 100644 index 0000000000..1301693ec7 --- /dev/null +++ b/plugins/main/src/main/res/xml/pref_garmin.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedListTest.kt b/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedListTest.kt new file mode 100644 index 0000000000..56e410c943 --- /dev/null +++ b/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedListTest.kt @@ -0,0 +1,192 @@ +package app.aaps.plugins.main.general.garmin + + +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.nio.ByteBuffer +import java.nio.ByteOrder + +internal class DeltaVarEncodedListTest { + + @Test fun empty() { + val l = DeltaVarEncodedList(100, 2) + assertArrayEquals(IntArray(0), l.toArray()) + } + + @Test fun add1() { + val l = DeltaVarEncodedList(100, 2) + l.add(10, 12) + assertArrayEquals(intArrayOf(10, 12), l.toArray()) + } + + @Test fun add2() { + val l = DeltaVarEncodedList(100, 2) + l.add(10, 16) + l.add(17, 9) + assertArrayEquals(intArrayOf(10, 16, 17, 9), l.toArray()) + } + + @Test fun add3() { + val l = DeltaVarEncodedList(100, 2) + l.add(10, 16) + l.add(17, 9) + l.add(-4, 5) + assertArrayEquals(intArrayOf(10, 16, 17, 9, -4, 5), l.toArray()) + } + + @Test fun decode() { + val bytes = ByteBuffer.allocate(6) + bytes.order(ByteOrder.LITTLE_ENDIAN) + bytes.putChar(65044.toChar()) + bytes.putChar(33026.toChar()) + bytes.putChar(4355.toChar()) + val l = DeltaVarEncodedList(intArrayOf(-1), bytes) + assertEquals(4, l.size.toLong()) + assertArrayEquals(intArrayOf(10, 201, 8, -1), l.toArray()) + } + + @Test fun decodeUneven() { + val bytes = ByteBuffer.allocate(8) + bytes.order(ByteOrder.LITTLE_ENDIAN) + bytes.putChar(65044.toChar()) + bytes.putChar(33026.toChar()) + bytes.putChar(59395.toChar()) + bytes.putChar(10.toChar()) + val l = DeltaVarEncodedList(intArrayOf(700), ByteBuffer.wrap(bytes.array(), 0, 7)) + assertEquals(4, l.size.toLong()) + assertArrayEquals(intArrayOf(10, 201, 8, 700), l.toArray()) + } + + @Test fun decodeInt() { + val bytes = ByteBuffer.allocate(8) + bytes.order(ByteOrder.LITTLE_ENDIAN) + bytes.putInt(-2130510316).putInt(714755) + val l = DeltaVarEncodedList(intArrayOf(700), ByteBuffer.wrap(bytes.array(), 0, 7)) + assertEquals(4, l.size.toLong()) + assertArrayEquals(intArrayOf(10, 201, 8, 700), l.toArray()) + } + + @Test fun decodeInt1() { + val bytes = ByteBuffer.allocate(3 * 4) + bytes.order(ByteOrder.LITTLE_ENDIAN) + bytes.putInt(-2019904035).putInt(335708683).putInt(529409) + val l = DeltaVarEncodedList(intArrayOf(1483884930, 132), ByteBuffer.wrap(bytes.array(), 0, 11)) + assertEquals(3, l.size.toLong()) + assertArrayEquals(intArrayOf(1483884910, 129, 1483884920, 128, 1483884930, 132), l.toArray()) + } + + @Test fun decodeInt2() { + val bytes = ByteBuffer.allocate(100) + bytes.order(ByteOrder.LITTLE_ENDIAN) + bytes + .putInt(-1761405951) + .putInt(335977999) + .putInt(335746050) + .putInt(336008197) + .putInt(335680514) + .putInt(335746053) + .putInt(-1761405949) + val l = DeltaVarEncodedList(intArrayOf(1483880370, 127), ByteBuffer.wrap(bytes.array(), 0, 28)) + assertEquals(12, l.size.toLong()) + assertArrayEquals( + intArrayOf( + 1483879986, + 999, + 1483879984, + 27, + 1483880383, + 37, + 1483880384, + 47, + 1483880382, + 57, + 1483880379, + 67, + 1483880375, + 77, + 1483880376, + 87, + 1483880377, + 97, + 1483880374, + 107, + 1483880372, + 117, + 1483880370, + 127 + ), + l.toArray() + ) + } + + @Test fun decodeInt3() { + val bytes = ByteBuffer.allocate(2 * 4) + bytes.order(ByteOrder.LITTLE_ENDIAN) + bytes.putInt(-2020427796).putInt(166411) + val l = DeltaVarEncodedList(intArrayOf(1483886070, 133), ByteBuffer.wrap(bytes.array(), 0, 7)) + assertEquals(1, l.size.toLong()) + assertArrayEquals(intArrayOf(1483886070, 133), l.toArray()) + } + + @Test fun decodePairs() { + val bytes = ByteBuffer.allocate(10) + bytes.order(ByteOrder.LITTLE_ENDIAN) + bytes.putChar(51220.toChar()) + bytes.putChar(65025.toChar()) + bytes.putChar(514.toChar()) + bytes.putChar(897.toChar()) + bytes.putChar(437.toChar()) + val l = DeltaVarEncodedList(intArrayOf(8, 10), bytes) + assertEquals(3, l.size.toLong()) + assertArrayEquals(intArrayOf(10, 100, 201, 101, 8, 10), l.toArray()) + } + + @Test fun encoding() { + val l = DeltaVarEncodedList(100, 2) + l.add(10, 16) + l.add(17, 9) + l.add(-4, 5) + val dataList = l.encodedData() + val byteBuffer = ByteBuffer.allocate(dataList.size * 8) + byteBuffer.order(ByteOrder.LITTLE_ENDIAN) + val longBuffer = byteBuffer.asLongBuffer() + for (i in dataList.indices) { + longBuffer.put(dataList[i]) + } + byteBuffer.rewind() + byteBuffer.limit(l.byteSize) + val l2 = DeltaVarEncodedList(intArrayOf(-4, 5), byteBuffer) + assertArrayEquals(intArrayOf(10, 16, 17, 9, -4, 5), l2.toArray()) + } + + @Test fun encoding2() { + val l = DeltaVarEncodedList(100, 2) + val values = intArrayOf( + 1511636926, 137, 1511637226, 138, 1511637526, 138, 1511637826, 137, 1511638126, 136, + 1511638426, 135, 1511638726, 134, 1511639026, 132, 1511639326, 130, 1511639626, 128, + 1511639926, 126, 1511640226, 124, 1511640526, 121, 1511640826, 118, 1511641127, 117, + 1511641427, 116, 1511641726, 115, 1511642027, 113, 1511642326, 111, 1511642627, 109, + 1511642927, 107, 1511643227, 107, 1511643527, 107, 1511643827, 106, 1511644127, 105, + 1511644427, 104, 1511644727, 104, 1511645027, 104, 1511645327, 104, 1511645626, 104, + 1511645926, 104, 1511646226, 105, 1511646526, 106, 1511646826, 107, 1511647126, 109, + 1511647426, 108 + ) + + for(i in values.indices step 2) { + l.add(values[i], values[i + 1]) + } + assertArrayEquals(values, l.toArray()) + val dataList = l.encodedData() + val byteBuffer = ByteBuffer.allocate(dataList.size * 8) + byteBuffer.order(ByteOrder.LITTLE_ENDIAN) + val longBuffer = byteBuffer.asLongBuffer() + for (i in dataList.indices) { + longBuffer.put(dataList[i]) + } + byteBuffer.rewind() + byteBuffer.limit(l.byteSize) + val l2 = DeltaVarEncodedList(intArrayOf(1511647426, 108), byteBuffer) + assertArrayEquals(values, l2.toArray()) + } +} diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/GarminPluginTest.kt b/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/GarminPluginTest.kt new file mode 100644 index 0000000000..31aae82f97 --- /dev/null +++ b/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/GarminPluginTest.kt @@ -0,0 +1,116 @@ +package app.aaps.plugins.main.general.garmin + +import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.rx.events.EventNewBG +import app.aaps.core.interfaces.sharedPreferences.SP +import app.aaps.database.entities.GlucoseValue +import app.aaps.shared.tests.TestBase +import dagger.android.AndroidInjector +import dagger.android.HasAndroidInjector +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.Mockito.atMost +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` +import java.net.URI +import java.time.Clock +import java.time.Instant +import java.time.ZoneId +import java.time.temporal.ChronoUnit +import java.util.concurrent.locks.Condition + +class GarminPluginTest: TestBase() { + private lateinit var gp: GarminPlugin + + @Mock private lateinit var rh: ResourceHelper + @Mock private lateinit var sp: SP + @Mock private lateinit var loopHub: LoopHub + private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC")) + + private var injector: HasAndroidInjector = HasAndroidInjector { + AndroidInjector { + } + } + + @BeforeEach + fun setup() { + gp = GarminPlugin(injector, aapsLogger, rh, loopHub, rxBus, sp) + gp.clock = clock + `when`(loopHub.currentProfileName).thenReturn("Default") + } + + @AfterEach + fun verifyNoFurtherInteractions() { + verify(loopHub, atMost(2)).currentProfileName + verifyNoMoreInteractions(loopHub) + } + + private val getGlucoseValuesFrom = clock.instant() + .minus(2, ChronoUnit.HOURS) + .minus(9, ChronoUnit.MINUTES) + + private fun createUri(params: Map): URI { + return URI("http://foo?" + params.entries.joinToString(separator = "&") { (k, v) -> + "$k=$v"}) + } + + private fun createHeartRate(@Suppress("SameParameterValue") heartRate: Int) = mapOf( + "hr" to heartRate, + "hrStart" to 1001L, + "hrEnd" to 2001L, + "device" to "Test_Device") + + private fun createGlucoseValue(timestamp: Instant, value: Double = 93.0) = GlucoseValue( + timestamp = timestamp.toEpochMilli(), raw = 90.0, value = value, + trendArrow = GlucoseValue.TrendArrow.FLAT, noise = null, + sourceSensor = GlucoseValue.SourceSensor.RANDOM + ) + + @Test + fun testReceiveHeartRateUri() { + val hr = createHeartRate(99) + val uri = createUri(hr) + gp.receiveHeartRate(uri) + verify(loopHub).storeHeartRate( + Instant.ofEpochSecond(hr["hrStart"] as Long), + Instant.ofEpochSecond(hr["hrEnd"] as Long), + 99, + hr["device"] as String) + } + + @Test + fun testReceiveHeartRate_UriTestIsTrue() { + val params = createHeartRate(99).toMutableMap() + params["test"] = true + val uri = createUri(params) + gp.receiveHeartRate(uri) + } + + @Test + fun testGetGlucoseValues_NoLast() { + val from = getGlucoseValuesFrom + val prev = createGlucoseValue(clock.instant().minusSeconds(310)) + `when`(loopHub.getGlucoseValues(from, true)).thenReturn(listOf(prev)) + assertArrayEquals(arrayOf(prev), gp.getGlucoseValues().toTypedArray()) + verify(loopHub).getGlucoseValues(from, true) + } + + @Test + fun testGetGlucoseValues_NoNewLast() { + val from = getGlucoseValuesFrom + val lastTimesteamp = clock.instant() + val prev = createGlucoseValue(clock.instant()) + gp.newValue = mock(Condition::class.java) + `when`(loopHub.getGlucoseValues(from, true)).thenReturn(listOf(prev)) + gp.onNewBloodGlucose(EventNewBG(lastTimesteamp.toEpochMilli())) + assertArrayEquals(arrayOf(prev), gp.getGlucoseValues().toTypedArray()) + + verify(gp.newValue).signalAll() + verify(loopHub).getGlucoseValues(from, true) + } +} diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/HttpServerTest.kt b/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/HttpServerTest.kt new file mode 100644 index 0000000000..8219e476cb --- /dev/null +++ b/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/HttpServerTest.kt @@ -0,0 +1,99 @@ +package app.aaps.plugins.main.general.garmin + +import app.aaps.shared.tests.TestBase +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.SocketAddress +import java.net.URI +import java.nio.charset.Charset +import java.time.Duration + +internal class HttpServerTest: TestBase() { + + private fun toInputStream(s: String): InputStream { + return ByteArrayInputStream(s.toByteArray(Charset.forName("ASCII"))) + } + + @Test fun testReadBody() { + val input = toInputStream("Test") + assertEquals("Test", HttpServer.readBody(input, 100)) + } + + @Test fun testReadBody_MoreContentThanLength() { + val input = toInputStream("Test") + assertEquals("Tes", HttpServer.readBody(input, 3)) + } + + @Test fun testParseRequest_Get() { + val req = """ + GET http://foo HTTP/1.1 + """.trimIndent() + assertEquals( + URI("http://foo") to null, + HttpServer.parseRequest(toInputStream(req))) + } + + @Test fun testParseRequest_PostEmptyBody() { + val req = """ + POST http://foo HTTP/1.1 + """.trimIndent() + assertEquals( + URI("http://foo") to null, + HttpServer.parseRequest(toInputStream(req))) + } + + @Test fun testParseRequest_PostBody() { + val req = """ + POST http://foo HTTP/1.1 + Content-Type: application/x-www-form-urlencoded + + a=1&b=2 + """.trimIndent() + assertEquals( + URI("http://foo?a=1&b=2") to null, + HttpServer.parseRequest(toInputStream(req))) + } + + @Test fun testParseRequest_PostBodyContentLength() { + val req = """ + POST http://foo HTTP/1.1 + Content-Type: application/x-www-form-urlencoded + Content-Length: 3 + + a=1&b=2 + """.trimIndent() + assertEquals( + URI("http://foo?a=1") to null, + HttpServer.parseRequest(toInputStream(req))) + } + + @Test fun testRequest() { + val port = 28895 + val reqUri = URI("http://127.0.0.1:$port/foo") + HttpServer(aapsLogger, port).use { server -> + server.registerEndpoint("/foo") { _: SocketAddress, uri: URI, _: String? -> + assertEquals(URI("/foo"), uri) + "test" + } + assertTrue(server.awaitReady(Duration.ofSeconds(10))) + val resp = reqUri.toURL().openConnection() as HttpURLConnection + assertEquals(200, resp.responseCode) + val content = (resp.content as InputStream).reader().use { r -> r.readText() } + assertEquals("test", content) + } + } + + @Test fun testRequest_NotFound() { + val port = 28895 + val reqUri = URI("http://127.0.0.1:$port/foo") + HttpServer(aapsLogger, port).use { server -> + assertTrue(server.awaitReady(Duration.ofSeconds(10))) + val resp = reqUri.toURL().openConnection() as HttpURLConnection + assertEquals(404, resp.responseCode) + } + } +} diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/LoopHubTest.kt b/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/LoopHubTest.kt new file mode 100644 index 0000000000..a812f0cbef --- /dev/null +++ b/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/LoopHubTest.kt @@ -0,0 +1,201 @@ +package app.aaps.plugins.main.general.garmin + + +import app.aaps.core.interfaces.aps.APSResult +import app.aaps.core.interfaces.aps.Loop +import app.aaps.core.interfaces.constraints.ConstraintsChecker +import app.aaps.core.interfaces.db.GlucoseUnit +import app.aaps.core.interfaces.iob.IobCobCalculator +import app.aaps.core.interfaces.iob.IobTotal +import app.aaps.core.interfaces.logging.UserEntryLogger +import app.aaps.core.interfaces.profile.Profile +import app.aaps.core.interfaces.profile.ProfileFunction +import app.aaps.core.interfaces.queue.CommandQueue +import app.aaps.database.ValueWrapper +import app.aaps.database.entities.EffectiveProfileSwitch +import app.aaps.database.entities.GlucoseValue +import app.aaps.database.entities.HeartRate +import app.aaps.database.entities.embedments.InsulinConfiguration +import app.aaps.database.impl.AppRepository +import app.aaps.database.impl.transactions.InsertOrUpdateHeartRateTransaction +import app.aaps.shared.tests.TestBase +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Single +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.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.`when` +import java.time.Clock +import java.time.Instant +import java.time.ZoneId + +class LoopHubTest: TestBase() { + @Mock lateinit var commandQueue: CommandQueue + @Mock lateinit var constraints: ConstraintsChecker + @Mock lateinit var iobCobCalculator: IobCobCalculator + @Mock lateinit var loop: Loop + @Mock lateinit var profileFunction: ProfileFunction + @Mock lateinit var repo: AppRepository + @Mock lateinit var userEntryLogger: UserEntryLogger + + private lateinit var loopHub: LoopHubImpl + private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC")) + + @BeforeEach + fun setup() { + loopHub = LoopHubImpl(iobCobCalculator, loop, profileFunction, repo) + loopHub.clock = clock + } + + @AfterEach + fun verifyNoFurtherInteractions() { + verifyNoMoreInteractions(commandQueue) + verifyNoMoreInteractions(constraints) + verifyNoMoreInteractions(iobCobCalculator) + verifyNoMoreInteractions(loop) + verifyNoMoreInteractions(profileFunction) + verifyNoMoreInteractions(repo) + verifyNoMoreInteractions(userEntryLogger) + } + + @Test + fun testCurrentProfile() { + val profile = mock(Profile::class.java) + `when`(profileFunction.getProfile()).thenReturn(profile) + assertEquals(profile, loopHub.currentProfile) + verify(profileFunction, times(1)).getProfile() + } + + @Test + fun testCurrentProfileName() { + `when`(profileFunction.getProfileName()).thenReturn("pro") + assertEquals("pro", loopHub.currentProfileName) + verify(profileFunction, times(1)).getProfileName() + } + + @Test + fun testGlucoseUnit() { + val profile = mock(Profile::class.java) + `when`(profile.units).thenReturn(GlucoseUnit.MMOL) + `when`(profileFunction.getProfile()).thenReturn(profile) + assertEquals(GlucoseUnit.MMOL, loopHub.glucoseUnit) + verify(profileFunction, times(1)).getProfile() + } + + @Test + fun testGlucoseUnitNullProfile() { + `when`(profileFunction.getProfile()).thenReturn(null) + assertEquals(GlucoseUnit.MGDL, loopHub.glucoseUnit) + verify(profileFunction, times(1)).getProfile() + } + + @Test + fun testInsulinOnBoard() { + val iobTotal = IobTotal(time = 0).apply { iob = 23.9 } + `when`(iobCobCalculator.calculateIobFromBolus()).thenReturn(iobTotal) + assertEquals(23.9, loopHub.insulinOnboard, 1e-10) + verify(iobCobCalculator, times(1)).calculateIobFromBolus() + } + + @Test + fun testIsConnected() { + `when`(loop.isDisconnected).thenReturn(false) + assertEquals(true, loopHub.isConnected) + verify(loop, times(1)).isDisconnected + } + + private fun effectiveProfileSwitch(duration: Long) = EffectiveProfileSwitch( + timestamp = 100, + basalBlocks = emptyList(), + isfBlocks = emptyList(), + icBlocks = emptyList(), + targetBlocks = emptyList(), + glucoseUnit = EffectiveProfileSwitch.GlucoseUnit.MGDL, + originalProfileName = "foo", + originalCustomizedName = "bar", + originalTimeshift = 0, + originalPercentage = 100, + originalDuration = duration, + originalEnd = 100 + duration, + insulinConfiguration = InsulinConfiguration( + "label", 0, 0 + ) + ) + + @Test + fun testIsTemporaryProfileTrue() { + val eps = effectiveProfileSwitch(10) + `when`(repo.getEffectiveProfileSwitchActiveAt(clock.millis())).thenReturn( + Single.just(ValueWrapper.Existing(eps))) + assertEquals(true, loopHub.isTemporaryProfile) + verify(repo, times(1)).getEffectiveProfileSwitchActiveAt(clock.millis()) + } + + @Test + fun testIsTemporaryProfileFalse() { + val eps = effectiveProfileSwitch(0) + `when`(repo.getEffectiveProfileSwitchActiveAt(clock.millis())).thenReturn( + Single.just(ValueWrapper.Existing(eps))) + assertEquals(false, loopHub.isTemporaryProfile) + verify(repo).getEffectiveProfileSwitchActiveAt(clock.millis()) + } + + @Test + fun testTemporaryBasal() { + val apsResult = mock(APSResult::class.java) + `when`(apsResult.percent).thenReturn(45) + val lastRun = Loop.LastRun().apply { constraintsProcessed = apsResult } + `when`(loop.lastRun).thenReturn(lastRun) + assertEquals(0.45, loopHub.temporaryBasal, 1e-6) + verify(loop).lastRun + } + + @Test + fun testTemporaryBasalNoRun() { + `when`(loop.lastRun).thenReturn(null) + assertTrue(loopHub.temporaryBasal.isNaN()) + verify(loop, times(1)).lastRun + } + + @Test + fun testGetGlucoseValues() { + val glucoseValues = listOf( + GlucoseValue( + timestamp = 1_000_000L, raw = 90.0, value = 93.0, + trendArrow = GlucoseValue.TrendArrow.FLAT, noise = null, + sourceSensor = GlucoseValue.SourceSensor.DEXCOM_G5_XDRIP)) + `when`(repo.compatGetBgReadingsDataFromTime(1001_000, false)) + .thenReturn(Single.just(glucoseValues)) + assertArrayEquals( + glucoseValues.toTypedArray(), + loopHub.getGlucoseValues(Instant.ofEpochMilli(1001_000), false).toTypedArray()) + verify(repo).compatGetBgReadingsDataFromTime(1001_000, false) + } + + @Test + fun testStoreHeartRate() { + val samplingStart = Instant.ofEpochMilli(1_001_000) + val samplingEnd = Instant.ofEpochMilli(1_101_000) + val hr = HeartRate( + timestamp = samplingStart.toEpochMilli(), + duration = samplingEnd.toEpochMilli() - samplingStart.toEpochMilli(), + dateCreated = clock.millis(), + beatsPerMinute = 101.0, + device = "Test Device") + `when`(repo.runTransaction(InsertOrUpdateHeartRateTransaction(hr))).thenReturn( + Completable.fromCallable { + InsertOrUpdateHeartRateTransaction.TransactionResult( + emptyList(), emptyList())}) + loopHub.storeHeartRate( + samplingStart, samplingEnd, 101, "Test Device") + verify(repo).runTransaction(InsertOrUpdateHeartRateTransaction(hr)) + } +} \ No newline at end of file From cde1be75ba189fbf73c61623db85f7e4d2cc62e9 Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 12 Oct 2023 00:36:54 +0200 Subject: [PATCH 07/22] Draft Refactor Communication --- .../maintenance/PrefFileListProvider.kt | 3 +- .../rx/events/EventMobileDataToWear.kt | 2 +- .../rx/weardata/CustomWatchfaceFormat.kt | 44 ++++++++++++++----- .../core/interfaces/rx/weardata/EventData.kt | 7 ++- core/utils/src/main/res/values/keys.xml | 1 + .../maintenance/PrefFileListProviderImpl.kt | 13 +++--- .../CustomWatchfaceImportListActivity.kt | 19 +++++--- .../plugins/main/general/wear/WearPlugin.kt | 10 +++-- .../wear/wearintegration/DataHandlerMobile.kt | 19 ++++++++ .../DataLayerListenerServiceMobile.kt | 3 +- .../app/aaps/wear/comm/DataHandlerWear.kt | 14 +++++- .../wear/comm/DataLayerListenerServiceWear.kt | 11 +++-- .../wear/interaction/utils/Persistence.kt | 38 ++++++++++++++++ .../aaps/wear/watchfaces/CustomWatchface.kt | 7 ++- 14 files changed, 150 insertions(+), 41 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefFileListProvider.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefFileListProvider.kt index ed0f3745cc..263cf8e052 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefFileListProvider.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefFileListProvider.kt @@ -1,6 +1,7 @@ package app.aaps.core.interfaces.maintenance import app.aaps.core.interfaces.rx.weardata.CwfData +import app.aaps.core.interfaces.rx.weardata.CwfFile import java.io.File interface PrefFileListProvider { @@ -13,7 +14,7 @@ interface PrefFileListProvider { fun newExportCsvFile(): File fun newCwfFile(filename: String, withDate: Boolean = true): File fun listPreferenceFiles(loadMetadata: Boolean = false): MutableList - fun listCustomWatchfaceFiles(): MutableList + fun listCustomWatchfaceFiles(): MutableList fun checkMetadata(metadata: Map): Map fun formatExportedAgo(utcTime: String): String } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileDataToWear.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileDataToWear.kt index 6f3ef85b9b..05a7bfcec8 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileDataToWear.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileDataToWear.kt @@ -2,4 +2,4 @@ package app.aaps.core.interfaces.rx.events import app.aaps.core.interfaces.rx.weardata.EventData -class EventMobileDataToWear(val payload: EventData.ActionSetCustomWatchface) : Event() \ No newline at end of file +class EventMobileDataToWear(val payload: ByteArray) : Event() \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt index d20837ca88..4ef514e29a 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt @@ -12,6 +12,7 @@ import com.caverock.androidsvg.SVG import kotlinx.serialization.Serializable import org.json.JSONObject import java.io.BufferedOutputStream +import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream @@ -134,10 +135,18 @@ data class ResData(val value: ByteArray, val format: ResFormat) { typealias CwfResDataMap = MutableMap typealias CwfMetadataMap = MutableMap -fun CwfResDataMap.isEquals(dataMap: CwfResDataMap) = (this.size == dataMap.size) && this.all { (key, resData) -> dataMap[key]?.value.contentEquals(resData.value) == true } +fun CwfResDataMap.sameRes(dataMap: CwfResDataMap) = (this.size == dataMap.size) && this.all { (key, resData) -> dataMap[key]?.value.contentEquals(resData.value) == true } +fun CwfMetadataMap.sameMeta(metaDataMap: CwfMetadataMap) = (this.size == metaDataMap.size) && this.all { (key, string) -> metaDataMap[key] == string } @Serializable -data class CwfData(val json: String, var metadata: CwfMetadataMap, val resDatas: CwfResDataMap) +data class CwfData(val json: String, var metadata: CwfMetadataMap, val resDatas: CwfResDataMap) { + fun simplify(): CwfData? = resDatas[ResFileMap.CUSTOM_WATCHFACE.fileName]?.let { + val simplifiedDatas: CwfResDataMap = mutableMapOf() + simplifiedDatas[ResFileMap.CUSTOM_WATCHFACE.fileName] = it + CwfData(json, metadata, simplifiedDatas) + } +} +data class CwfFile(val cwfData: CwfData, val zipByteArray: ByteArray) enum class CwfMetadataKey(val key: String, @StringRes val label: Int, val isPref: Boolean) { @@ -286,24 +295,24 @@ class ZipWatchfaceFormat { const val CWF_EXTENTION = ".zip" const val CWF_JSON_FILE = "CustomWatchface.json" - fun loadCustomWatchface(zipInputStream: ZipInputStream, zipName: String, authorization: Boolean): CwfData? { + fun loadCustomWatchface(zipInputStream: ZipInputStream, zipName: String, authorization: Boolean): CwfFile? { var json = JSONObject() var metadata: CwfMetadataMap = mutableMapOf() val resDatas: CwfResDataMap = mutableMapOf() - + val testZip = byteArrayToZipInputStream(zipInputStreamToByteArray(zipInputStream)) try { - var zipEntry: ZipEntry? = zipInputStream.nextEntry + var zipEntry: ZipEntry? = testZip.nextEntry while (zipEntry != null) { val entryName = zipEntry.name val buffer = ByteArray(2048) val byteArrayOutputStream = ByteArrayOutputStream() - var count = zipInputStream.read(buffer) + var count = testZip.read(buffer) while (count != -1) { byteArrayOutputStream.write(buffer, 0, count) - count = zipInputStream.read(buffer) + count = testZip.read(buffer) } - zipInputStream.closeEntry() + testZip.closeEntry() if (entryName == CWF_JSON_FILE) { val jsonString = byteArrayOutputStream.toByteArray().toString(Charsets.UTF_8) @@ -319,12 +328,12 @@ class ZipWatchfaceFormat { } else if (drawableFormat != ResFormat.UNKNOWN) resDatas[entryName.substringBeforeLast(".")] = ResData(byteArrayOutputStream.toByteArray(), drawableFormat) } - zipEntry = zipInputStream.nextEntry + zipEntry = testZip.nextEntry } // Valid CWF file must contains a valid json file with a name within metadata and a custom watchface image return if (metadata.containsKey(CwfMetadataKey.CWF_NAME) && resDatas.containsKey(ResFileMap.CUSTOM_WATCHFACE.fileName)) - CwfData(json.toString(4), metadata, resDatas) + CwfFile(CwfData(json.toString(4), metadata, resDatas), zipInputStreamToByteArray(zipInputStream)) else null @@ -368,5 +377,20 @@ class ZipWatchfaceFormat { } return metadata } + + fun zipInputStreamToByteArray(zipInputStream: ZipInputStream): ByteArray { + val buffer = ByteArray(1024) + val byteArrayOutputStream = ByteArrayOutputStream() + var len: Int + while (zipInputStream.read(buffer).also { len = it } > 0) { + byteArrayOutputStream.write(buffer, 0, len) + } + return byteArrayOutputStream.toByteArray() + } + + fun byteArrayToZipInputStream(byteArray: ByteArray): ZipInputStream { + val byteArrayInputStream = ByteArrayInputStream(byteArray) + return ZipInputStream(byteArrayInputStream) + } } } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt index e20a0796b9..744712d139 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt @@ -292,10 +292,9 @@ sealed class EventData : Event() { } @Serializable - data class ActionSetCustomWatchface( - val customWatchfaceData: CwfData - ) : EventData() - + data class ActionSetCustomWatchface(val customWatchfaceData: CwfData) : EventData() + @Serializable + data class ActionUpdateCustomWatchface(val customWatchfaceData: CwfData) : EventData() @Serializable data class ActionrequestCustomWatchface(val exportFile: Boolean) : EventData() diff --git a/core/utils/src/main/res/values/keys.xml b/core/utils/src/main/res/values/keys.xml index f8e845faa9..f8d1c22142 100644 --- a/core/utils/src/main/res/values/keys.xml +++ b/core/utils/src/main/res/values/keys.xml @@ -116,6 +116,7 @@ wearwizard_cob wearwizard_iob wear_custom_watchface_autorization + wear_custom_watchface_save_cwfdata ObjectivesbgIsAvailableInNS ObjectivespumpStatusIsAvailableInNS statuslights_cage_warning diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt index fe466ef4e9..291470136d 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt @@ -12,6 +12,7 @@ import app.aaps.core.interfaces.maintenance.PrefsMetadataKey import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.weardata.CwfData +import app.aaps.core.interfaces.rx.weardata.CwfFile import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.rx.weardata.ZipWatchfaceFormat import app.aaps.core.interfaces.sharedPreferences.SP @@ -97,11 +98,11 @@ class PrefFileListProviderImpl @Inject constructor( return prefFiles } - override fun listCustomWatchfaceFiles(): MutableList { - val customWatchfaceFiles = mutableListOf() - val customAwtchfaceAuthorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) + override fun listCustomWatchfaceFiles(): MutableList { + val customWatchfaceFiles = mutableListOf() + val customWatchfaceAuthorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) exportsPath.walk().filter { it.isFile && it.name.endsWith(ZipWatchfaceFormat.CWF_EXTENTION) }.forEach { file -> - ZipWatchfaceFormat.loadCustomWatchface(ZipInputStream(file.inputStream()), file.name, customAwtchfaceAuthorization)?.also { customWatchface -> + ZipWatchfaceFormat.loadCustomWatchface(ZipInputStream(file.inputStream()), file.name, customWatchfaceAuthorization)?.also { customWatchface -> customWatchfaceFiles.add(customWatchface) } } @@ -111,9 +112,9 @@ class PrefFileListProviderImpl @Inject constructor( for (assetFileName in assetFiles) { if (assetFileName.endsWith(ZipWatchfaceFormat.CWF_EXTENTION)) { val assetInputStream = context.assets.open(assetFileName) - ZipWatchfaceFormat.loadCustomWatchface(ZipInputStream(assetInputStream), assetFileName, customAwtchfaceAuthorization)?.also { customWatchface -> + ZipWatchfaceFormat.loadCustomWatchface(ZipInputStream(assetInputStream), assetFileName, customWatchfaceAuthorization)?.also { customWatchface -> customWatchfaceFiles.add(customWatchface) - rxBus.send(EventData.ActionGetCustomWatchface(EventData.ActionSetCustomWatchface(customWatchface), exportFile = true, withDate = false)) + //rxBus.send(EventData.ActionGetCustomWatchface(EventData.ActionSetCustomWatchface(customWatchface.cwfData), exportFile = true, withDate = false)) } assetInputStream.close() } diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt index f42849b993..b200ea53be 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt @@ -15,6 +15,7 @@ import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventMobileDataToWear import app.aaps.core.interfaces.rx.weardata.CUSTOM_VERSION import app.aaps.core.interfaces.rx.weardata.CwfData +import app.aaps.core.interfaces.rx.weardata.CwfFile import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_AUTHOR import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_AUTHOR_VERSION import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_CREATED_AT @@ -22,6 +23,7 @@ import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_FILENAME import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_NAME import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_VERSION import app.aaps.core.interfaces.rx.weardata.CwfMetadataMap +import app.aaps.core.interfaces.rx.weardata.CwfResDataMap import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.rx.weardata.ResFileMap import app.aaps.core.interfaces.rx.weardata.ZipWatchfaceFormat @@ -56,10 +58,10 @@ class CustomWatchfaceImportListActivity : TranslatedDaggerAppCompatActivity() { supportActionBar?.setDisplayShowTitleEnabled(true) binding.recyclerview.layoutManager = LinearLayoutManager(this) - binding.recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listCustomWatchfaceFiles().sortedBy { it.metadata[CWF_NAME] }) + binding.recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listCustomWatchfaceFiles().sortedBy { it.cwfData.metadata[CWF_NAME] }) } - inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List) : RecyclerView.Adapter() { + inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List) : RecyclerView.Adapter() { inner class CwfFileViewHolder(val customWatchfaceImportListItemBinding: CustomWatchfaceImportListItemBinding) : RecyclerView.ViewHolder(customWatchfaceImportListItemBinding.root) { @@ -67,11 +69,14 @@ class CustomWatchfaceImportListActivity : TranslatedDaggerAppCompatActivity() { with(customWatchfaceImportListItemBinding) { root.isClickable = true customWatchfaceImportListItemBinding.root.setOnClickListener { - val customWatchfaceFile = filelistName.tag as CwfData - val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile) + val customWatchfaceFile = filelistName.tag as CwfFile + val cwfData = CwfData(customWatchfaceFile.cwfData.json, customWatchfaceFile.cwfData.metadata, mutableMapOf()) + //Save json and metadata + sp.putString(app.aaps.core.utils.R.string.key_wear_custom_watchface_save_cwfData, EventData.ActionSetCustomWatchface(cwfData).serialize()) val i = Intent() setResult(FragmentActivity.RESULT_OK, i) - rxBus.send(EventMobileDataToWear(customWF)) + rxBus.send(EventMobileDataToWear(customWatchfaceFile.zipByteArray)) + aapsLogger.debug("XXXXX: ${customWatchfaceFile.zipByteArray.size}") finish() } } @@ -89,8 +94,8 @@ class CustomWatchfaceImportListActivity : TranslatedDaggerAppCompatActivity() { override fun onBindViewHolder(holder: CwfFileViewHolder, position: Int) { val customWatchfaceFile = customWatchfaceFileList[position] - val metadata = customWatchfaceFile.metadata - val drawable = customWatchfaceFile.resDatas[ResFileMap.CUSTOM_WATCHFACE.fileName]?.toDrawable(resources) + val metadata = customWatchfaceFile.cwfData.metadata + val drawable = customWatchfaceFile.cwfData.resDatas[ResFileMap.CUSTOM_WATCHFACE.fileName]?.toDrawable(resources) with(holder.customWatchfaceImportListItemBinding) { val fileName = metadata[CWF_FILENAME]?.let { "$it${ZipWatchfaceFormat.CWF_EXTENTION}" } ?: "" filelistName.text = rh.gs(app.aaps.core.interfaces.R.string.metadata_wear_import_filename, fileName) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt index 391a731c9c..c02def89b9 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt @@ -114,10 +114,12 @@ class WearPlugin @Inject constructor( savedCustomWatchface?.let { cwf -> val cwf_authorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) if (cwf_authorization != cwf.metadata[CwfMetadataKey.CWF_AUTHORIZATION]?.toBooleanStrictOrNull()) { - // resend new customWatchface to Watch with updated authorization for preferences update - val newCwf = cwf.copy() - newCwf.metadata[CwfMetadataKey.CWF_AUTHORIZATION] = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false).toString() - rxBus.send(EventMobileDataToWear(EventData.ActionSetCustomWatchface(newCwf))) + // update new customWatchface to Watch with updated authorization for preferences update + CwfData(cwf.json, cwf.metadata, mutableMapOf()).also { + it.metadata[CwfMetadataKey.CWF_AUTHORIZATION] = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false).toString() + sp.putString(app.aaps.core.utils.R.string.key_wear_custom_watchface_save_cwfData, EventData.ActionSetCustomWatchface(it).serialize()) + rxBus.send(EventMobileToWear(EventData.ActionUpdateCustomWatchface(it))) + } } } } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt index c55939ecbc..09355413aa 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt @@ -31,6 +31,7 @@ import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventMobileToWear import app.aaps.core.interfaces.rx.events.EventWearUpdateGui +import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.ui.UiInteraction @@ -70,6 +71,7 @@ import app.aaps.plugins.main.R import dagger.android.HasAndroidInjector import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign +import org.json.JSONObject import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date @@ -1267,6 +1269,23 @@ class DataHandlerMobile @Inject constructor( private fun handleGetCustomWatchface(command: EventData.ActionGetCustomWatchface) { val customWatchface = command.customWatchface aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.customWatchfaceData.json}") + try { + + var s = sp.getStringOrNull(app.aaps.core.utils.R.string.key_wear_custom_watchface_save_cwfData, null) + if (s != null) { + (EventData.deserialize(s) as EventData.ActionSetCustomWatchface).also { savedCwData -> + if (customWatchface.customWatchfaceData.json != savedCwData.customWatchfaceData.json && + customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] && + customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] + ) { + // if different json but same name and author version, then resync json and metadata to watch to update filename and authorization + rxBus.send(EventMobileToWear(EventData.ActionUpdateCustomWatchface(savedCwData.customWatchfaceData))) + } + } + } + } catch (exception: Exception) { + aapsLogger.error(LTag.WEAR, exception.toString()) + } rxBus.send(EventWearUpdateGui(customWatchface.customWatchfaceData, command.exportFile)) if (command.exportFile) importExportPrefs.exportCustomWatchface(customWatchface.customWatchfaceData, command.withDate) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt index ec3685409c..ff7254effa 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt @@ -93,7 +93,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() { disposable += rxBus .toObservable(EventMobileDataToWear::class.java) .observeOn(aapsSchedulers.io) - .subscribe { sendMessage(rxDataPath, it.payload.serializeByte()) } + .subscribe { sendMessage(rxDataPath, it.payload) } } override fun onCapabilityChanged(p0: CapabilityInfo) { @@ -214,6 +214,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() { private fun sendMessage(path: String, data: ByteArray) { aapsLogger.debug(LTag.WEAR, "sendMessage: $path") + aapsLogger.debug("XXXXX: $path, ${data.size}") transcriptionNodeId?.also { nodeId -> messageClient .sendMessage(nodeId, path, data).apply { diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt index 119fc92587..038f2124f8 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt @@ -186,7 +186,17 @@ class DataHandlerWear @Inject constructor( .subscribe { aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${it.sourceNodeId}") persistence.store(it) - persistence.readCustomWatchface()?.let { + persistence.readSimplifiedCwf()?.let { + rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, false))) + } + } + disposable += rxBus + .toObservable(EventData.ActionUpdateCustomWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { + aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${it.sourceNodeId}") + persistence.store(it) + persistence.readSimplifiedCwf()?.let { rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, false))) } } @@ -205,7 +215,7 @@ class DataHandlerWear @Inject constructor( .observeOn(aapsSchedulers.io) .subscribe { eventData -> aapsLogger.debug(LTag.WEAR, "Custom Watchface requested from ${eventData.sourceNodeId} export ${eventData.exportFile}") - persistence.readCustomWatchface(eventData.exportFile)?.let { + persistence.readSimplifiedCwf(eventData.exportFile)?.let { rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, eventData.exportFile))) } } diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt index 11914555ad..348a705a06 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt @@ -12,6 +12,7 @@ import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventWearDataToMobile import app.aaps.core.interfaces.rx.events.EventWearToMobile import app.aaps.core.interfaces.rx.weardata.EventData +import app.aaps.core.interfaces.rx.weardata.ZipWatchfaceFormat import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.wear.interaction.utils.Persistence import app.aaps.wear.interaction.utils.WearUtil @@ -127,9 +128,13 @@ class DataLayerListenerServiceWear : WearableListenerService() { } rxDataPath -> { - aapsLogger.debug(LTag.WEAR, "onMessageReceived: ${messageEvent.data}") - val command = EventData.deserializeByte(messageEvent.data) - rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) + aapsLogger.debug(LTag.WEAR, "onMessageReceived: ${messageEvent.data.size}") + ZipWatchfaceFormat.loadCustomWatchface(ZipWatchfaceFormat.byteArrayToZipInputStream(messageEvent.data), "NewWatchface", false)?.let { + val command = EventData.ActionSetCustomWatchface(it.cwfData) + rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) + + aapsLogger.debug("XXXXX: ${it.cwfData.json}") + } // Use this sender transcriptionNodeId = messageEvent.sourceNodeId aapsLogger.debug(LTag.WEAR, "Updated node: $transcriptionNodeId") diff --git a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt index 67dcf70797..d9a1630926 100644 --- a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt +++ b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt @@ -3,6 +3,9 @@ package app.aaps.wear.interaction.utils import app.aaps.annotations.OpenForTesting import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.rx.events.EventMobileToWear +import app.aaps.core.interfaces.rx.weardata.CwfData +import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.rx.weardata.EventData.Companion.deserialize import app.aaps.core.interfaces.rx.weardata.EventData.SingleBg @@ -149,6 +152,26 @@ open class Persistence @Inject constructor( return null } + fun readSimplifiedCwf(isDefault: Boolean = false): EventData.ActionSetCustomWatchface? { + try { + var s = sp.getStringOrNull(if (isDefault) CUSTOM_DEFAULT_WATCHFACE else CUSTOM_WATCHFACE, null) + if (s != null) { + return (deserialize(s) as EventData.ActionSetCustomWatchface).let { + EventData.ActionSetCustomWatchface(it.customWatchfaceData.simplify() ?:it.customWatchfaceData) + } + + } else { + s = sp.getStringOrNull(CUSTOM_DEFAULT_WATCHFACE, null) + if (s != null) { + return deserialize(s) as EventData.ActionSetCustomWatchface + } + } + } catch (exception: Exception) { + aapsLogger.error(LTag.WEAR, exception.toString()) + } + return null + } + fun store(singleBg: SingleBg) { putString(BG_DATA_PERSISTENCE_KEY, singleBg.serialize()) aapsLogger.debug(LTag.WEAR, "Stored BG data: $singleBg") @@ -175,6 +198,21 @@ open class Persistence @Inject constructor( aapsLogger.debug(LTag.WEAR, "Stored Custom Watchface ${customWatchface.customWatchfaceData} ${isdefault}: $customWatchface") } + fun store(customWatchface: EventData.ActionUpdateCustomWatchface) { + readCustomWatchface()?.let { savedCwData -> + if (customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] && + customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] + ) { + // if different json but same name and author version, then resync json and metadata to watch to update filename and authorization + val newCwfData = CwfData(customWatchface.customWatchfaceData.json, customWatchface.customWatchfaceData.metadata, savedCwData.customWatchfaceData.resDatas) + EventData.ActionSetCustomWatchface(newCwfData).also { + putString(CUSTOM_WATCHFACE, it.serialize()) + aapsLogger.debug(LTag.WEAR, "Update Custom Watchface ${it.customWatchfaceData} : $customWatchface") + } + } + } +} + fun setDefaultWatchface() { readCustomWatchface(true)?.let { store(it) } aapsLogger.debug(LTag.WEAR, "Custom Watchface reset to default") diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index 345812cb27..c2f9b6df52 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -44,7 +44,8 @@ import app.aaps.core.interfaces.rx.weardata.ResFileMap import app.aaps.core.interfaces.rx.weardata.ResFormat import app.aaps.core.interfaces.rx.weardata.ViewKeys import app.aaps.core.interfaces.rx.weardata.ZipWatchfaceFormat -import app.aaps.core.interfaces.rx.weardata.isEquals +import app.aaps.core.interfaces.rx.weardata.sameMeta +import app.aaps.core.interfaces.rx.weardata.sameRes import app.aaps.wear.R import app.aaps.wear.databinding.ActivityCustomBinding import app.aaps.wear.watchfaces.utils.BaseWatchFace @@ -66,6 +67,7 @@ class CustomWatchface : BaseWatchFace() { private var lowBatColor = Color.RED private var resDataMap: CwfResDataMap = mutableMapOf() private var json = JSONObject() + private var metadata: CwfMetadataMap = mutableMapOf() private var jsonString = "" private val bgColor: Int get() = when (singleBg.sgvLevel) { @@ -153,8 +155,9 @@ class CustomWatchface : BaseWatchFace() { updatePref(it.customWatchfaceData.metadata) try { json = JSONObject(it.customWatchfaceData.json) - if (!resDataMap.isEquals(it.customWatchfaceData.resDatas) || jsonString != it.customWatchfaceData.json) { + if (!resDataMap.sameRes(it.customWatchfaceData.resDatas) || !metadata.sameMeta(it.customWatchfaceData.metadata) || jsonString != it.customWatchfaceData.json) { resDataMap = it.customWatchfaceData.resDatas + metadata = it.customWatchfaceData.metadata jsonString = it.customWatchfaceData.json FontMap.init(this) ViewMap.init(this) From 9cc7ff5cc27bc9f9ebe4a805117c03ebdea6c3f9 Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 12 Oct 2023 22:23:32 +0200 Subject: [PATCH 08/22] Wear CWF Refactor communication --- .../rx/weardata/CustomWatchfaceFormat.kt | 30 ++++++------------- core/utils/src/main/res/values/keys.xml | 4 ++- .../maintenance/PrefFileListProviderImpl.kt | 9 +++--- .../CustomWatchfaceImportListActivity.kt | 8 ++--- .../plugins/main/general/wear/WearPlugin.kt | 24 ++++++++++----- .../wear/wearintegration/DataHandlerMobile.kt | 30 +++++++------------ .../DataLayerListenerServiceMobile.kt | 1 - .../wear/comm/DataLayerListenerServiceWear.kt | 4 +-- .../wear/interaction/utils/Persistence.kt | 4 +-- .../aaps/wear/watchfaces/CustomWatchface.kt | 7 ++--- 10 files changed, 52 insertions(+), 69 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt index 4ef514e29a..cab1b1787b 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt @@ -135,9 +135,7 @@ data class ResData(val value: ByteArray, val format: ResFormat) { typealias CwfResDataMap = MutableMap typealias CwfMetadataMap = MutableMap -fun CwfResDataMap.sameRes(dataMap: CwfResDataMap) = (this.size == dataMap.size) && this.all { (key, resData) -> dataMap[key]?.value.contentEquals(resData.value) == true } -fun CwfMetadataMap.sameMeta(metaDataMap: CwfMetadataMap) = (this.size == metaDataMap.size) && this.all { (key, string) -> metaDataMap[key] == string } - +fun CwfResDataMap.isEquals(dataMap: CwfResDataMap) = (this.size == dataMap.size) && this.all { (key, resData) -> dataMap[key]?.value.contentEquals(resData.value) == true } @Serializable data class CwfData(val json: String, var metadata: CwfMetadataMap, val resDatas: CwfResDataMap) { fun simplify(): CwfData? = resDatas[ResFileMap.CUSTOM_WATCHFACE.fileName]?.let { @@ -295,24 +293,24 @@ class ZipWatchfaceFormat { const val CWF_EXTENTION = ".zip" const val CWF_JSON_FILE = "CustomWatchface.json" - fun loadCustomWatchface(zipInputStream: ZipInputStream, zipName: String, authorization: Boolean): CwfFile? { + fun loadCustomWatchface(byteArray: ByteArray, zipName: String, authorization: Boolean): CwfFile? { var json = JSONObject() var metadata: CwfMetadataMap = mutableMapOf() val resDatas: CwfResDataMap = mutableMapOf() - val testZip = byteArrayToZipInputStream(zipInputStreamToByteArray(zipInputStream)) + val zipInputStream = byteArrayToZipInputStream(byteArray) try { - var zipEntry: ZipEntry? = testZip.nextEntry + var zipEntry: ZipEntry? = zipInputStream.nextEntry while (zipEntry != null) { val entryName = zipEntry.name val buffer = ByteArray(2048) val byteArrayOutputStream = ByteArrayOutputStream() - var count = testZip.read(buffer) + var count = zipInputStream.read(buffer) while (count != -1) { byteArrayOutputStream.write(buffer, 0, count) - count = testZip.read(buffer) + count = zipInputStream.read(buffer) } - testZip.closeEntry() + zipInputStream.closeEntry() if (entryName == CWF_JSON_FILE) { val jsonString = byteArrayOutputStream.toByteArray().toString(Charsets.UTF_8) @@ -328,12 +326,12 @@ class ZipWatchfaceFormat { } else if (drawableFormat != ResFormat.UNKNOWN) resDatas[entryName.substringBeforeLast(".")] = ResData(byteArrayOutputStream.toByteArray(), drawableFormat) } - zipEntry = testZip.nextEntry + zipEntry = zipInputStream.nextEntry } // Valid CWF file must contains a valid json file with a name within metadata and a custom watchface image return if (metadata.containsKey(CwfMetadataKey.CWF_NAME) && resDatas.containsKey(ResFileMap.CUSTOM_WATCHFACE.fileName)) - CwfFile(CwfData(json.toString(4), metadata, resDatas), zipInputStreamToByteArray(zipInputStream)) + CwfFile(CwfData(json.toString(4), metadata, resDatas), byteArray) else null @@ -378,16 +376,6 @@ class ZipWatchfaceFormat { return metadata } - fun zipInputStreamToByteArray(zipInputStream: ZipInputStream): ByteArray { - val buffer = ByteArray(1024) - val byteArrayOutputStream = ByteArrayOutputStream() - var len: Int - while (zipInputStream.read(buffer).also { len = it } > 0) { - byteArrayOutputStream.write(buffer, 0, len) - } - return byteArrayOutputStream.toByteArray() - } - fun byteArrayToZipInputStream(byteArray: ByteArray): ZipInputStream { val byteArrayInputStream = ByteArrayInputStream(byteArray) return ZipInputStream(byteArrayInputStream) diff --git a/core/utils/src/main/res/values/keys.xml b/core/utils/src/main/res/values/keys.xml index f8d1c22142..b98cdfe2c0 100644 --- a/core/utils/src/main/res/values/keys.xml +++ b/core/utils/src/main/res/values/keys.xml @@ -116,7 +116,9 @@ wearwizard_cob wearwizard_iob wear_custom_watchface_autorization - wear_custom_watchface_save_cwfdata + wear_cwf_watchface_name + wear_cwf_author_version + wear_cwf_filename ObjectivesbgIsAvailableInNS ObjectivespumpStatusIsAvailableInNS statuslights_cage_warning diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt index 291470136d..33f1162eca 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt @@ -102,7 +102,7 @@ class PrefFileListProviderImpl @Inject constructor( val customWatchfaceFiles = mutableListOf() val customWatchfaceAuthorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) exportsPath.walk().filter { it.isFile && it.name.endsWith(ZipWatchfaceFormat.CWF_EXTENTION) }.forEach { file -> - ZipWatchfaceFormat.loadCustomWatchface(ZipInputStream(file.inputStream()), file.name, customWatchfaceAuthorization)?.also { customWatchface -> + ZipWatchfaceFormat.loadCustomWatchface(file.readBytes(), file.name, customWatchfaceAuthorization)?.also { customWatchface -> customWatchfaceFiles.add(customWatchface) } } @@ -111,12 +111,11 @@ class PrefFileListProviderImpl @Inject constructor( val assetFiles = context.assets.list("") ?: arrayOf() for (assetFileName in assetFiles) { if (assetFileName.endsWith(ZipWatchfaceFormat.CWF_EXTENTION)) { - val assetInputStream = context.assets.open(assetFileName) - ZipWatchfaceFormat.loadCustomWatchface(ZipInputStream(assetInputStream), assetFileName, customWatchfaceAuthorization)?.also { customWatchface -> + val assetByteArray = context.assets.open(assetFileName).readBytes() + ZipWatchfaceFormat.loadCustomWatchface(assetByteArray, assetFileName, customWatchfaceAuthorization)?.also { customWatchface -> customWatchfaceFiles.add(customWatchface) - //rxBus.send(EventData.ActionGetCustomWatchface(EventData.ActionSetCustomWatchface(customWatchface.cwfData), exportFile = true, withDate = false)) + rxBus.send(EventData.ActionGetCustomWatchface(EventData.ActionSetCustomWatchface(customWatchface.cwfData), exportFile = true, withDate = false)) } - assetInputStream.close() } } } catch (e: Exception) { diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt index b200ea53be..3858a4135d 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt @@ -70,13 +70,13 @@ class CustomWatchfaceImportListActivity : TranslatedDaggerAppCompatActivity() { root.isClickable = true customWatchfaceImportListItemBinding.root.setOnClickListener { val customWatchfaceFile = filelistName.tag as CwfFile - val cwfData = CwfData(customWatchfaceFile.cwfData.json, customWatchfaceFile.cwfData.metadata, mutableMapOf()) - //Save json and metadata - sp.putString(app.aaps.core.utils.R.string.key_wear_custom_watchface_save_cwfData, EventData.ActionSetCustomWatchface(cwfData).serialize()) + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_watchface_name, customWatchfaceFile.cwfData.metadata[CWF_NAME] ?:"") + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_author_version, customWatchfaceFile.cwfData.metadata[CWF_AUTHOR_VERSION] ?:"") + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_filename, customWatchfaceFile.cwfData.metadata[CWF_FILENAME] ?:"") + val i = Intent() setResult(FragmentActivity.RESULT_OK, i) rxBus.send(EventMobileDataToWear(customWatchfaceFile.zipByteArray)) - aapsLogger.debug("XXXXX: ${customWatchfaceFile.zipByteArray.size}") finish() } } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt index c02def89b9..46cea0082b 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt @@ -11,7 +11,6 @@ import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventAutosensCalculationFinished import app.aaps.core.interfaces.rx.events.EventDismissBolusProgressIfRunning import app.aaps.core.interfaces.rx.events.EventLoopUpdateGui -import app.aaps.core.interfaces.rx.events.EventMobileDataToWear import app.aaps.core.interfaces.rx.events.EventMobileToWear import app.aaps.core.interfaces.rx.events.EventOverviewBolusProgress import app.aaps.core.interfaces.rx.events.EventPreferenceChange @@ -112,14 +111,23 @@ class WearPlugin @Inject constructor( fun checkCustomWatchfacePreferences() { savedCustomWatchface?.let { cwf -> - val cwf_authorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) - if (cwf_authorization != cwf.metadata[CwfMetadataKey.CWF_AUTHORIZATION]?.toBooleanStrictOrNull()) { - // update new customWatchface to Watch with updated authorization for preferences update - CwfData(cwf.json, cwf.metadata, mutableMapOf()).also { - it.metadata[CwfMetadataKey.CWF_AUTHORIZATION] = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false).toString() - sp.putString(app.aaps.core.utils.R.string.key_wear_custom_watchface_save_cwfData, EventData.ActionSetCustomWatchface(it).serialize()) - rxBus.send(EventMobileToWear(EventData.ActionUpdateCustomWatchface(it))) + val cwfAuthorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) + val cwfName = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_watchface_name, "") + val authorVersion = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_author_version, "") + val fileName = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_filename, "") + var toUpdate = false + CwfData("", cwf.metadata, mutableMapOf()).also { + if (cwfAuthorization != cwf.metadata[CwfMetadataKey.CWF_AUTHORIZATION]?.toBooleanStrictOrNull()) { + it.metadata[CwfMetadataKey.CWF_AUTHORIZATION] = cwfAuthorization.toString() + toUpdate = true } + if (cwfName == cwf.metadata[CwfMetadataKey.CWF_NAME] && authorVersion == cwf.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] && fileName != cwf.metadata[CwfMetadataKey.CWF_FILENAME]) { + it.metadata[CwfMetadataKey.CWF_FILENAME] = fileName + toUpdate = true + } + + if (toUpdate) + rxBus.send(EventMobileToWear(EventData.ActionUpdateCustomWatchface(it))) } } } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt index 09355413aa..7c69e06b5b 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt @@ -1268,27 +1268,19 @@ class DataHandlerMobile @Inject constructor( private fun handleGetCustomWatchface(command: EventData.ActionGetCustomWatchface) { val customWatchface = command.customWatchface - aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.customWatchfaceData.json}") - try { - - var s = sp.getStringOrNull(app.aaps.core.utils.R.string.key_wear_custom_watchface_save_cwfData, null) - if (s != null) { - (EventData.deserialize(s) as EventData.ActionSetCustomWatchface).also { savedCwData -> - if (customWatchface.customWatchfaceData.json != savedCwData.customWatchfaceData.json && - customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] && - customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] - ) { - // if different json but same name and author version, then resync json and metadata to watch to update filename and authorization - rxBus.send(EventMobileToWear(EventData.ActionUpdateCustomWatchface(savedCwData.customWatchfaceData))) - } - } - } - } catch (exception: Exception) { - aapsLogger.error(LTag.WEAR, exception.toString()) + aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}") + val cwfData = customWatchface.customWatchfaceData + rxBus.send(EventWearUpdateGui(cwfData, command.exportFile)) + val watchfaceName = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_watchface_name, "") + val authorVersion = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_author_version, "") + if (cwfData.metadata[CwfMetadataKey.CWF_NAME] != watchfaceName || cwfData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] != authorVersion) { + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_watchface_name, cwfData.metadata[CwfMetadataKey.CWF_NAME] ?:"") + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_author_version, cwfData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] ?:"") + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_filename, cwfData.metadata[CwfMetadataKey.CWF_FILENAME] ?:"") } - rxBus.send(EventWearUpdateGui(customWatchface.customWatchfaceData, command.exportFile)) + if (command.exportFile) - importExportPrefs.exportCustomWatchface(customWatchface.customWatchfaceData, command.withDate) + importExportPrefs.exportCustomWatchface(cwfData, command.withDate) } } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt index ff7254effa..da33906a80 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt @@ -214,7 +214,6 @@ class DataLayerListenerServiceMobile : WearableListenerService() { private fun sendMessage(path: String, data: ByteArray) { aapsLogger.debug(LTag.WEAR, "sendMessage: $path") - aapsLogger.debug("XXXXX: $path, ${data.size}") transcriptionNodeId?.also { nodeId -> messageClient .sendMessage(nodeId, path, data).apply { diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt index 348a705a06..c701b69391 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt @@ -129,11 +129,9 @@ class DataLayerListenerServiceWear : WearableListenerService() { rxDataPath -> { aapsLogger.debug(LTag.WEAR, "onMessageReceived: ${messageEvent.data.size}") - ZipWatchfaceFormat.loadCustomWatchface(ZipWatchfaceFormat.byteArrayToZipInputStream(messageEvent.data), "NewWatchface", false)?.let { + ZipWatchfaceFormat.loadCustomWatchface(messageEvent.data, "", false)?.let { val command = EventData.ActionSetCustomWatchface(it.cwfData) rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) - - aapsLogger.debug("XXXXX: ${it.cwfData.json}") } // Use this sender transcriptionNodeId = messageEvent.sourceNodeId diff --git a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt index d9a1630926..dcf074a80a 100644 --- a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt +++ b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt @@ -203,8 +203,8 @@ open class Persistence @Inject constructor( if (customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] && customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] ) { - // if different json but same name and author version, then resync json and metadata to watch to update filename and authorization - val newCwfData = CwfData(customWatchface.customWatchfaceData.json, customWatchface.customWatchfaceData.metadata, savedCwData.customWatchfaceData.resDatas) + // if same name and author version, then resync metadata to watch to update filename and authorization + val newCwfData = CwfData(savedCwData.customWatchfaceData.json, customWatchface.customWatchfaceData.metadata, savedCwData.customWatchfaceData.resDatas) EventData.ActionSetCustomWatchface(newCwfData).also { putString(CUSTOM_WATCHFACE, it.serialize()) aapsLogger.debug(LTag.WEAR, "Update Custom Watchface ${it.customWatchfaceData} : $customWatchface") diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index c2f9b6df52..345812cb27 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -44,8 +44,7 @@ import app.aaps.core.interfaces.rx.weardata.ResFileMap import app.aaps.core.interfaces.rx.weardata.ResFormat import app.aaps.core.interfaces.rx.weardata.ViewKeys import app.aaps.core.interfaces.rx.weardata.ZipWatchfaceFormat -import app.aaps.core.interfaces.rx.weardata.sameMeta -import app.aaps.core.interfaces.rx.weardata.sameRes +import app.aaps.core.interfaces.rx.weardata.isEquals import app.aaps.wear.R import app.aaps.wear.databinding.ActivityCustomBinding import app.aaps.wear.watchfaces.utils.BaseWatchFace @@ -67,7 +66,6 @@ class CustomWatchface : BaseWatchFace() { private var lowBatColor = Color.RED private var resDataMap: CwfResDataMap = mutableMapOf() private var json = JSONObject() - private var metadata: CwfMetadataMap = mutableMapOf() private var jsonString = "" private val bgColor: Int get() = when (singleBg.sgvLevel) { @@ -155,9 +153,8 @@ class CustomWatchface : BaseWatchFace() { updatePref(it.customWatchfaceData.metadata) try { json = JSONObject(it.customWatchfaceData.json) - if (!resDataMap.sameRes(it.customWatchfaceData.resDatas) || !metadata.sameMeta(it.customWatchfaceData.metadata) || jsonString != it.customWatchfaceData.json) { + if (!resDataMap.isEquals(it.customWatchfaceData.resDatas) || jsonString != it.customWatchfaceData.json) { resDataMap = it.customWatchfaceData.resDatas - metadata = it.customWatchfaceData.metadata jsonString = it.customWatchfaceData.json FontMap.init(this) ViewMap.init(this) From 6ee6b8b976abab0fbdfdc151a70d37714cacc4c8 Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Thu, 12 Oct 2023 23:54:41 +0200 Subject: [PATCH 09/22] Move GarminPlugin to sync package. --- .../kotlin/app/aaps/activities/MyPreferenceFragment.kt | 2 +- app/src/main/kotlin/app/aaps/di/PluginsListModule.kt | 2 +- .../kotlin/app/aaps/plugins/main/di/PluginsModule.kt | 2 -- .../aaps/plugins/main/general/garmin/GarminModule.kt | 10 ---------- plugins/main/src/main/res/values/strings.xml | 3 --- .../main/kotlin/app/aaps/plugins/sync/di/SyncModule.kt | 3 +++ .../aaps/plugins/sync}/garmin/DeltaVarEncodedList.kt | 2 +- .../app/aaps/plugins/sync}/garmin/GarminPlugin.kt | 6 +++--- .../kotlin/app/aaps/plugins/sync}/garmin/HttpServer.kt | 2 +- .../kotlin/app/aaps/plugins/sync}/garmin/LoopHub.kt | 2 +- .../app/aaps/plugins/sync}/garmin/LoopHubImpl.kt | 2 +- plugins/sync/src/main/res/values/strings.xml | 4 ++++ .../{main => sync}/src/main/res/xml/pref_garmin.xml | 0 .../plugins/sync}/garmin/DeltaVarEncodedListTest.kt | 2 +- .../app/aaps/plugins/sync}/garmin/GarminPluginTest.kt | 2 +- .../app/aaps/plugins/sync}/garmin/HttpServerTest.kt | 2 +- .../app/aaps/plugins/sync}/garmin/LoopHubTest.kt | 2 +- 17 files changed, 20 insertions(+), 28 deletions(-) delete mode 100644 plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminModule.kt rename plugins/{main/src/main/kotlin/app/aaps/plugins/main/general => sync/src/main/kotlin/app/aaps/plugins/sync}/garmin/DeltaVarEncodedList.kt (99%) rename plugins/{main/src/main/kotlin/app/aaps/plugins/main/general => sync/src/main/kotlin/app/aaps/plugins/sync}/garmin/GarminPlugin.kt (98%) rename plugins/{main/src/main/kotlin/app/aaps/plugins/main/general => sync/src/main/kotlin/app/aaps/plugins/sync}/garmin/HttpServer.kt (99%) rename plugins/{main/src/main/kotlin/app/aaps/plugins/main/general => sync/src/main/kotlin/app/aaps/plugins/sync}/garmin/LoopHub.kt (96%) rename plugins/{main/src/main/kotlin/app/aaps/plugins/main/general => sync/src/main/kotlin/app/aaps/plugins/sync}/garmin/LoopHubImpl.kt (98%) rename plugins/{main => sync}/src/main/res/xml/pref_garmin.xml (100%) rename plugins/{main/src/test/kotlin/app/aaps/plugins/main/general => sync/src/test/kotlin/app/aaps/plugins/sync}/garmin/DeltaVarEncodedListTest.kt (99%) rename plugins/{main/src/test/kotlin/app/aaps/plugins/main/general => sync/src/test/kotlin/app/aaps/plugins/sync}/garmin/GarminPluginTest.kt (98%) rename plugins/{main/src/test/kotlin/app/aaps/plugins/main/general => sync/src/test/kotlin/app/aaps/plugins/sync}/garmin/HttpServerTest.kt (98%) rename plugins/{main/src/test/kotlin/app/aaps/plugins/main/general => sync/src/test/kotlin/app/aaps/plugins/sync}/garmin/LoopHubTest.kt (99%) diff --git a/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt b/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt index 0d46f6a925..407c135495 100644 --- a/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt @@ -43,7 +43,7 @@ import app.aaps.plugins.configuration.maintenance.MaintenancePlugin import app.aaps.plugins.constraints.safety.SafetyPlugin import app.aaps.plugins.insulin.InsulinOrefFreePeakPlugin import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin -import app.aaps.plugins.main.general.garmin.GarminPlugin +import app.aaps.plugins.sync.garmin.GarminPlugin import app.aaps.plugins.main.general.wear.WearPlugin import app.aaps.plugins.sensitivity.SensitivityAAPSPlugin import app.aaps.plugins.sensitivity.SensitivityOref1Plugin diff --git a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt index 8430e9eab3..02ab7bd4c6 100644 --- a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt +++ b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt @@ -22,7 +22,7 @@ import app.aaps.plugins.insulin.InsulinOrefRapidActingPlugin import app.aaps.plugins.insulin.InsulinOrefUltraRapidActingPlugin import app.aaps.plugins.main.general.actions.ActionsPlugin import app.aaps.plugins.main.general.food.FoodPlugin -import app.aaps.plugins.main.general.garmin.GarminPlugin +import app.aaps.plugins.sync.garmin.GarminPlugin import app.aaps.plugins.main.general.overview.OverviewPlugin import app.aaps.plugins.main.general.persistentNotification.PersistentNotificationPlugin import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt index bb02312d79..0d9b1f930b 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/PluginsModule.kt @@ -2,7 +2,6 @@ package app.aaps.plugins.main.di import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.smsCommunicator.SmsCommunicator -import app.aaps.plugins.main.general.garmin.GarminModule import app.aaps.plugins.main.general.persistentNotification.DummyService import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin import app.aaps.plugins.main.general.wear.WearFragment @@ -24,7 +23,6 @@ import dagger.android.ContributesAndroidInjector ActionsModule::class, WearModule::class, OverviewModule::class, - GarminModule::class, ] ) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminModule.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminModule.kt deleted file mode 100644 index 255ceceadb..0000000000 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminModule.kt +++ /dev/null @@ -1,10 +0,0 @@ -package app.aaps.plugins.main.general.garmin - -import dagger.Binds -import dagger.Module - -@Module -abstract class GarminModule { - @Suppress("unused") - @Binds abstract fun bindLoopHub(loopHub: LoopHubImpl): LoopHub -} \ No newline at end of file diff --git a/plugins/main/src/main/res/values/strings.xml b/plugins/main/src/main/res/values/strings.xml index 18af69163d..9f1b4c6556 100644 --- a/plugins/main/src/main/res/values/strings.xml +++ b/plugins/main/src/main/res/values/strings.xml @@ -401,8 +401,5 @@ DEFAULT RANGE target Rate: %1$.2fU/h (%2$.2f%%) \nDuration %3$d min - Garmin - Connection to Garmin device (Fenix, Edge, …) - Garmin settings diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/di/SyncModule.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/di/SyncModule.kt index aa8fd81445..409af55495 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/di/SyncModule.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/di/SyncModule.kt @@ -7,6 +7,8 @@ import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData import app.aaps.core.interfaces.nsclient.StoreDataForDb import app.aaps.core.interfaces.sync.DataSyncSelectorXdrip import app.aaps.core.interfaces.sync.XDripBroadcast +import app.aaps.plugins.sync.garmin.LoopHub +import app.aaps.plugins.sync.garmin.LoopHubImpl import app.aaps.plugins.sync.nsShared.NSClientFragment import app.aaps.plugins.sync.nsShared.StoreDataForDbImpl import app.aaps.plugins.sync.nsclient.data.NSSettingsStatusImpl @@ -82,6 +84,7 @@ abstract class SyncModule { @Binds fun bindDataSyncSelectorXdripInterface(dataSyncSelectorXdripImpl: DataSyncSelectorXdripImpl): DataSyncSelectorXdrip @Binds fun bindStoreDataForDb(storeDataForDbImpl: StoreDataForDbImpl): StoreDataForDb @Binds fun bindXDripBroadcastInterface(xDripBroadcastImpl: XdripPlugin): XDripBroadcast + @Binds fun bindLoopHub(loopHub: LoopHubImpl): LoopHub } } \ No newline at end of file diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedList.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedList.kt similarity index 99% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedList.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedList.kt index 9934bd0215..ab82262a2b 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedList.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedList.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.garmin +package app.aaps.plugins.sync.garmin import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt similarity index 98% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminPlugin.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index 6a87777301..da1d4f084e 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.garmin +package app.aaps.plugins.sync.garmin import androidx.annotation.VisibleForTesting import app.aaps.core.interfaces.db.GlucoseUnit @@ -13,7 +13,7 @@ import app.aaps.core.interfaces.rx.events.EventNewBG 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.main.R +import app.aaps.plugins.sync.R import com.google.gson.JsonObject import dagger.android.HasAndroidInjector import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -47,7 +47,7 @@ class GarminPlugin @Inject constructor( private val sp: SP, ) : PluginBase( PluginDescription() - .mainType(PluginType.GENERAL) + .mainType(PluginType.SYNC) .pluginName(R.string.garmin) .shortName(R.string.garmin) .description(R.string.garmin_description) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/HttpServer.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt similarity index 99% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/HttpServer.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt index fa44d60597..de327c909c 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/HttpServer.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.garmin +package app.aaps.plugins.sync.garmin import android.os.StrictMode import androidx.annotation.VisibleForTesting diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHub.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt similarity index 96% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHub.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt index 6397049377..69f2f44a62 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHub.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.garmin +package app.aaps.plugins.sync.garmin import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.profile.Profile diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHubImpl.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt similarity index 98% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHubImpl.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt index 672b5a86c3..e762d27b49 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/garmin/LoopHubImpl.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.garmin +package app.aaps.plugins.sync.garmin import androidx.annotation.VisibleForTesting import app.aaps.core.interfaces.aps.Loop diff --git a/plugins/sync/src/main/res/values/strings.xml b/plugins/sync/src/main/res/values/strings.xml index 72f0a1e164..1d16bfab7c 100644 --- a/plugins/sync/src/main/res/values/strings.xml +++ b/plugins/sync/src/main/res/values/strings.xml @@ -182,4 +182,8 @@ Data Broadcaster Broadcast data to other apps like Garmin watch + + Garmin + Connection to Garmin device (Fenix, Edge, …) + Garmin settings \ No newline at end of file diff --git a/plugins/main/src/main/res/xml/pref_garmin.xml b/plugins/sync/src/main/res/xml/pref_garmin.xml similarity index 100% rename from plugins/main/src/main/res/xml/pref_garmin.xml rename to plugins/sync/src/main/res/xml/pref_garmin.xml diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedListTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedListTest.kt similarity index 99% rename from plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedListTest.kt rename to plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedListTest.kt index 56e410c943..1902fdc4ec 100644 --- a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/DeltaVarEncodedListTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedListTest.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.garmin +package app.aaps.plugins.sync.garmin import org.junit.jupiter.api.Assertions.assertArrayEquals diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt similarity index 98% rename from plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/GarminPluginTest.kt rename to plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt index 31aae82f97..c0af17edfa 100644 --- a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/GarminPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.garmin +package app.aaps.plugins.sync.garmin import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.events.EventNewBG diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/HttpServerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt similarity index 98% rename from plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/HttpServerTest.kt rename to plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt index 8219e476cb..d89dfa9156 100644 --- a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/HttpServerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.garmin +package app.aaps.plugins.sync.garmin import app.aaps.shared.tests.TestBase import org.junit.jupiter.api.Assertions.assertEquals diff --git a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/LoopHubTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt similarity index 99% rename from plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/LoopHubTest.kt rename to plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt index a812f0cbef..6f1cccad86 100644 --- a/plugins/main/src/test/kotlin/app/aaps/plugins/main/general/garmin/LoopHubTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.garmin +package app.aaps.plugins.sync.garmin import app.aaps.core.interfaces.aps.APSResult From 378bde46c9a262980eb4981662d000055b3c47b9 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 13 Oct 2023 18:41:26 +0200 Subject: [PATCH 10/22] Wear CWF Manage Background Color of textView (Static and dynData) --- .../aaps/wear/watchfaces/CustomWatchface.kt | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index 345812cb27..6e1e0a05e9 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -106,13 +106,13 @@ class CustomWatchface : BaseWatchFace() { override fun setColorDark() { setWatchfaceStyle() - if ((ViewMap.SGV.dynData?.stepColor ?: 0) == 0) + if ((ViewMap.SGV.dynData?.stepFontColor ?: 0) == 0) binding.sgv.setTextColor(bgColor) if ((ViewMap.DIRECTION.dynData?.stepColor ?: 0) == 0) binding.direction2.colorFilter = changeDrawableColor(bgColor) - if (ageLevel != 1 && (ViewMap.TIMESTAMP.dynData?.stepColor ?: 0) == 0) + if (ageLevel != 1 && (ViewMap.TIMESTAMP.dynData?.stepFontColor ?: 0) == 0) binding.timestamp.setTextColor(ContextCompat.getColor(this, R.color.dark_TimestampOld)) - if (status.batteryLevel != 1 && (ViewMap.UPLOADER_BATTERY.dynData?.stepColor ?: 0) == 0) + if (status.batteryLevel != 1 && (ViewMap.UPLOADER_BATTERY.dynData?.stepFontColor ?: 0) == 0) binding.uploaderBattery.setTextColor(lowBatColor) if ((ViewMap.LOOP.dynData?.stepDraw ?: 0) == 0) // Apply automatic background image only if no dynData or no step images when (loopLevel) { @@ -508,11 +508,17 @@ class CustomWatchface : BaseWatchFace() { FontMap.font(viewJson.optString(FONT.key, FontMap.DEFAULT.key)), StyleMap.style(viewJson.optString(FONTSTYLE.key, StyleMap.NORMAL.key)) ) - view.setTextColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(FONTCOLOR.key))) + view.setTextColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(FONTCOLOR.key))) view.isAllCaps = viewJson.optBoolean(ALLCAPS.key) if (viewJson.has(TEXTVALUE.key)) view.text = viewJson.optString(TEXTVALUE.key) - view.background = dynData?.getDrawable() ?: textDrawable() + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) + view.background = (dynData?.getDrawable() ?: textDrawable())?.also { + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files + it.colorFilter = cwf.changeDrawableColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + else + it.clearColorFilter() + } } ?: apply { view.text = "" } } @@ -522,14 +528,14 @@ class CustomWatchface : BaseWatchFace() { viewJson?.let { viewJson -> drawable?.let { if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files - it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + it.colorFilter = cwf.changeDrawableColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() view.setImageDrawable(it) } ?: apply { view.setImageDrawable(defaultDrawable?.let { cwf.resources.getDrawable(it) }) if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) - view.setColorFilter(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + view.setColorFilter(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else view.clearColorFilter() } @@ -540,7 +546,12 @@ class CustomWatchface : BaseWatchFace() { customizeViewCommon(view) viewJson?.let { viewJson -> view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) - view.background = dynData?.getDrawable() ?: textDrawable() + view.background = (dynData?.getDrawable() ?: textDrawable())?.also { + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files + it.colorFilter = cwf.changeDrawableColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + else + it.clearColorFilter() + } } } } @@ -695,6 +706,7 @@ class CustomWatchface : BaseWatchFace() { private val dynDrawable = mutableMapOf() private val dynColor = mutableMapOf() + private val dynFontColor = mutableMapOf() private var dataRange: DataRange? = null private var topRange: DataRange? = null private var leftRange: DataRange? = null @@ -703,6 +715,8 @@ class CustomWatchface : BaseWatchFace() { get() = dynDrawable.size - 1 val stepColor: Int get() = dynColor.size - 1 + val stepFontColor: Int + get() = dynFontColor.size - 1 val dataValue: Double? get() = when (valueMap) { @@ -727,6 +741,7 @@ class CustomWatchface : BaseWatchFace() { ?: (leftRange.invalidData * cwf.zoomFactor).toInt() } } ?: 0 fun getRotationOffset(): Int = dataRange?.let { dataRange -> rotationRange?.let { rotRange -> dataValue?.let { valueMap.dynValue(it, dataRange, rotRange) } ?: rotRange.invalidData } } ?: 0 fun getDrawable() = dataRange?.let { dataRange -> dataValue?.let { dynDrawable[valueMap.stepValue(it, dataRange, stepDraw)] } ?: dynDrawable[0] } + fun getFontColor() = if (stepFontColor > 0) dataRange?.let { dataRange -> dataValue?.let { dynFontColor[valueMap.stepValue(it, dataRange, stepFontColor)] } ?: dynFontColor[0] } else null fun getColor() = if (stepColor > 0) dataRange?.let { dataRange -> dataValue?.let { dynColor[valueMap.stepValue(it, dataRange, stepColor)] } ?: dynColor[0] } else null private fun load() { dynDrawable[0] = dataJson.optString(INVALIDIMAGE.key)?.let { cwf.resDataMap[it]?.toDrawable(cwf.resources, width, height) } @@ -741,6 +756,12 @@ class CustomWatchface : BaseWatchFace() { dynColor[idx] = cwf.getColor(dataJson.optString("${COLOR.key}$idx")) idx++ } + dynFontColor[0] = cwf.getColor(dataJson.optString(INVALIDFONTCOLOR.key)) + idx = 1 + while (dataJson.has("${FONTCOLOR.key}$idx")) { + dynFontColor[idx] = cwf.getColor(dataJson.optString("${FONTCOLOR.key}$idx")) + idx++ + } DataRange(dataJson.optDouble(MINDATA.key, valueMap.min), dataJson.optDouble(MAXDATA.key, valueMap.max)).let { defaultRange -> dataRange = defaultRange topRange = parseDataRange(dataJson.optJSONObject(TOPOFFSET.key), defaultRange) From 456ed9a1ceeedfd5a373c192601622b07fcfa931 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 13 Oct 2023 18:42:31 +0200 Subject: [PATCH 11/22] Wear CWF Manage Background Color of textView (Static and dynData) missing Key --- .../aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt index d20837ca88..a3a9ba6438 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt @@ -252,6 +252,7 @@ enum class JsonKeys(val key: String) { IMAGE("image"), INVALIDIMAGE("invalidImage"), INVALIDCOLOR("invalidColor"), + INVALIDFONTCOLOR("invalidFontColor"), TWINVIEW("twinView"), TOPOFFSETTWINHIDDEN("topOffsetTwinHidden"), LEFTOFFSETTWINHIDDEN("leftOffsetTwinHidden") From dad5d9906800dd45dd9dcd97b4b1d018559ff3f6 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 13 Oct 2023 21:07:36 +0200 Subject: [PATCH 12/22] Wear CWF Manage Background Color fix Background Color for all views including ImageView --- .../app/aaps/wear/watchfaces/CustomWatchface.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index 6e1e0a05e9..f8bd5ddb7a 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -512,12 +512,14 @@ class CustomWatchface : BaseWatchFace() { view.isAllCaps = viewJson.optBoolean(ALLCAPS.key) if (viewJson.has(TEXTVALUE.key)) view.text = viewJson.optString(TEXTVALUE.key) - view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) - view.background = (dynData?.getDrawable() ?: textDrawable())?.also { + (dynData?.getDrawable() ?: textDrawable())?.let { if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files it.colorFilter = cwf.changeDrawableColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() + view.background = it + } ?: apply { + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) } } ?: apply { view.text = "" } } @@ -539,18 +541,22 @@ class CustomWatchface : BaseWatchFace() { else view.clearColorFilter() } + if (view.drawable == null) + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) } } fun customizeGraphView(view: lecho.lib.hellocharts.view.LineChartView) { customizeViewCommon(view) viewJson?.let { viewJson -> - view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) - view.background = (dynData?.getDrawable() ?: textDrawable())?.also { + (dynData?.getDrawable() ?: textDrawable())?.let { if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files it.colorFilter = cwf.changeDrawableColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() + view.background = it + } ?: apply { + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) } } } From 9403607f1862460de144453ab0f6fe849e825597 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 13 Oct 2023 21:10:36 +0200 Subject: [PATCH 13/22] Wear CWF Remove SonarLint Warning --- .../kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index f8bd5ddb7a..e27d5923ef 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -431,7 +431,7 @@ class CustomWatchface : BaseWatchFace() { ); companion object { - + val TRANSPARENT = "#00000000" fun init(cwf: CustomWatchface) = values().forEach { it.cwf = cwf // reset all customized drawable when new watchface is loaded @@ -519,7 +519,7 @@ class CustomWatchface : BaseWatchFace() { it.clearColorFilter() view.background = it } ?: apply { - view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, TRANSPARENT), Color.TRANSPARENT)) } } ?: apply { view.text = "" } } @@ -542,7 +542,7 @@ class CustomWatchface : BaseWatchFace() { view.clearColorFilter() } if (view.drawable == null) - view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, TRANSPARENT), Color.TRANSPARENT)) } } @@ -556,7 +556,7 @@ class CustomWatchface : BaseWatchFace() { it.clearColorFilter() view.background = it } ?: apply { - view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, TRANSPARENT), Color.TRANSPARENT)) } } } From 103435a0d27e7d494938113546405efe3f51a210 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 13 Oct 2023 22:34:11 +0200 Subject: [PATCH 14/22] Wear CWF Fix getColor for images --- .../kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index e27d5923ef..d388e674c4 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -514,7 +514,7 @@ class CustomWatchface : BaseWatchFace() { view.text = viewJson.optString(TEXTVALUE.key) (dynData?.getDrawable() ?: textDrawable())?.let { if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files - it.colorFilter = cwf.changeDrawableColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() view.background = it @@ -530,14 +530,14 @@ class CustomWatchface : BaseWatchFace() { viewJson?.let { viewJson -> drawable?.let { if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files - it.colorFilter = cwf.changeDrawableColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() view.setImageDrawable(it) } ?: apply { view.setImageDrawable(defaultDrawable?.let { cwf.resources.getDrawable(it) }) if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) - view.setColorFilter(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + view.setColorFilter(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else view.clearColorFilter() } @@ -551,7 +551,7 @@ class CustomWatchface : BaseWatchFace() { viewJson?.let { viewJson -> (dynData?.getDrawable() ?: textDrawable())?.let { if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files - it.colorFilter = cwf.changeDrawableColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() view.background = it From 919682d0c4cf0f6ee6b9c391abf209a7b555fa01 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 13 Oct 2023 22:43:07 +0200 Subject: [PATCH 15/22] Wear CWF include some comments --- .../app/aaps/wear/watchfaces/CustomWatchface.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index d388e674c4..bdc91cb2f3 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -513,12 +513,12 @@ class CustomWatchface : BaseWatchFace() { if (viewJson.has(TEXTVALUE.key)) view.text = viewJson.optString(TEXTVALUE.key) (dynData?.getDrawable() ?: textDrawable())?.let { - if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) not for svg files it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() view.background = it - } ?: apply { + } ?: apply { // if no drawable loaded either background key or dynData, then apply color to text background view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, TRANSPARENT), Color.TRANSPARENT)) } } ?: apply { view.text = "" } @@ -529,19 +529,19 @@ class CustomWatchface : BaseWatchFace() { view.clearColorFilter() viewJson?.let { viewJson -> drawable?.let { - if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) not for svg files it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() view.setImageDrawable(it) } ?: apply { view.setImageDrawable(defaultDrawable?.let { cwf.resources.getDrawable(it) }) - if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // works on xml included into res files view.setColorFilter(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else view.clearColorFilter() } - if (view.drawable == null) + if (view.drawable == null) // if no drowable (either default, hardcoded or dynData, then apply color to background view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, TRANSPARENT), Color.TRANSPARENT)) } } @@ -550,12 +550,12 @@ class CustomWatchface : BaseWatchFace() { customizeViewCommon(view) viewJson?.let { viewJson -> (dynData?.getDrawable() ?: textDrawable())?.let { - if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) not for svg files it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() view.background = it - } ?: apply { + } ?: apply { // if no drowable loaded, then apply color to background view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, TRANSPARENT), Color.TRANSPARENT)) } } From e5ce6422c6d169ee5a6da5ec19c559ecff828986 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 13 Oct 2023 22:57:24 +0200 Subject: [PATCH 16/22] Wear CWF Update Steampunk in assets --- .../src/main/assets/SteamPunk mgdl.zip | Bin 308851 -> 308861 bytes .../src/main/assets/SteamPunk mmol.zip | Bin 298539 -> 298544 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/plugins/configuration/src/main/assets/SteamPunk mgdl.zip b/plugins/configuration/src/main/assets/SteamPunk mgdl.zip index a1d1ae1395c2bfe2330ee959b9cd26bc671858f1..f78a49f2401b063fe6500f4ca4efe5a6f22b13ba 100644 GIT binary patch delta 2781 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 delta 2781 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 diff --git a/plugins/configuration/src/main/assets/SteamPunk mmol.zip b/plugins/configuration/src/main/assets/SteamPunk mmol.zip index 1076b2afcfadd95907d00ba31278b59c399ad4b9..e15fb0fc2b290a04dda2ea7588f27bcbb5ee3fb1 100644 GIT binary patch delta 3090 zcmV+t4DIu)oD#5{5`eS;B}WY)woO;9EPmr^3jhGFmoi5IFn{~4X)o8}4!Gj7=zUor zpd?ymBawzg*>xBF@5dREda)(f=_SpkoAd<=Ux&{;Gec6NKYk+mXOJ;PW2)%jB{3yf zDrm-Z$>7zi_t)h3S}~fvFU9Q%$ugM^hIK<)s)fw4sfcs7CjX^*&PAf6Xf}&-Miq;1 zsDh@R|NF0g@PGTSf#+ZB4tBF*dBLTCmj1cd8fHD;S1YrO37p|oOqY5-5^l)*1y5y} z5-AAI>%qLiXn_5`nC^oGPZqCbYF6^Ihd*}cLB`|0s#LD`R9#Y4*ZzGndNG=~+KA?1 zU3YakdtLVoX+hIV8r>$jEXCH`S68iOul0IaUN@PJFMlUjQ`aQP`$b<$F4Q&u3~Kgm z851;2*)@x#h;^~bOIA(h#A)+n&11FDMPtuvC}P1hoNA|7$z?|KgoEyyXywUt5w`@ZUjR++v7q=v2YR#A+wQyv`H3g}D(7kBJBeJ4t$qEv)BFcHl2p78d z$3m0gGJlm30iMUiO+gh=3r3jkFX#G^7i7*;c8;akDTGcS2^UdX#=5UYQC0&$^)2Cr z9XFRsi+i1{P^2|aQ<70Z6J}~Oi(#II_%w=S9&7U zJ@CKh^MWZ9^*EF=WmKFP35=3k65vV=bbAD74}aa_2dZ(!3m)>6tIgo0{Vio}Gm929 zSKSN>{Am}1eD9)%LBXt-Lo@HIV&RHftTw{ccDE(D)PCotU;g{GfF9u!DYgKe=Zq=# zN%`MXC`W{?;y$3O1|Vo*JWk*@%9EU~Y_ACYS+mnzS~tMvv`p1@)wSBB7^#HJGn#H! z*MHvc@(uhMY<;bd;2tRzO74s&$M)bFuA- z=BBUil(Wib?f8@3wRE#A=rP@kPSb4{H-A|+9rw{}I{ra9R1T5UkAPHk7$e6C|wR zDw3%fwk9-D0J@ui($A{Np77C0v(ez0W_O`k2j;$aSJ21Lp#Q(12M2@BW!Mw3JAdQ# z!$)h}S@gg>dL)>)5$lOQvrfn+lOD{1X&-38xCgdi_N@%PGjzEOrII(W@nAy7hMQ2P z@izl$Pkb#Pqo+i@$FlHk#2%V@vmVsWekkh2;3WULZm$c^{mj#W<>nc*JAzg&Q%Pfe zSP!YvlWw}Pn|6ZNGPfpretlxz#(&ShxbL&E_dp=FPb4a*qFCy4p-?0OM9EzL13B;l zODetH*H48$BZMrX`lM2wTHr1u=P`rpRlN@~-HWd|1JB#pIaw9G(@v)_m1@n!2hP^$ zk$6%m(NxN%*;D4H=749YH4~5u6`>^*c=YR6apyppZK58-D+`!A=Clf8l-xsqh{Mo_m8Lq?O%5;mx zGhl}UmP)zodtcrJtU3bpA@(BZgUlcG90AUP@wXlVg71r5&*==ndzI&MU2GXGhhv{v zOC#LBZdPTCjQ%Z?H>3bpQ2lwY#_D$pFdyj#e=XGTY zEz{n6hKEVY=>}@&RCTkUecN6?=+z zKfq1bd4Z;&b?Jxc9{*&QAx!}-e}_pbIXNfqz~lsGC{dVhm5@?!WfGB^IZK!bW$G~2 z994pX(1p!$Y$rLw&7`gwR@aXiq;-l|5B$kUVCEUUz`>zLTbIeg%DBz-G|kO(n##3a zdB$B%WKOhOVZtRy=2Rr?oV<0HrGl+nb7H*#){I4oiHAhH_LN3p`+lO<@~CS{i8B!Y)hgjpO=N0fK7ybQairqMdb}1v?81yj`_(p(oLU zzr%F6+ifbZ7d%?HTsiIQf6|zQ__P*~2)EWmm*N*i7IX#JZwQWt?ALsq<0}s{7S{s9 zvJJzbytWD|w9a^;Ajc|ag?6bC+sX$1%$JxqJOr)?-QNm@(mIhW2zE`lBv_+^|Mn)K zWx{SC-~PrMwRJTbysR@9Ee6^{IBuU=7%eTiFq|#3kmbN;J;$;ze;4(|QVN$Y)^5Zd z$ORZPG2VcE(!!-UEuuJc>4i0Jg&7EfAke~ya6I<1|tS|t96(UAvRw6y?gk*7& zwpTlb)#5z9b3c`m*XA_^l+AfwC>)6#hYXBR)5f9nz)WY`#RR401tu{1|YW@m>!uq7yz6~0a=4Vx=zm=X=8hS*4L z5iuzA^2GO=?Scc_jrw^vX|@}GouAlla$vjZf%9G*xQ^Mrb~|2ja#!bg)#91U&O8RC zS%p=kF`O}ZzbDQ_;cZ*R9Ag&H3V+=;JAD5Jjdy+eki3H=e_%~1CWpE>R^3F~iR@F|BvwMu4ZAnK^2cg;-ujk~q z1*?{iW1wf!_?X>LE&a%Ifz@d3%=rUJwyREU)s{1ByG-j{wV{HjA3#FT;F;d^p`dN| z<4R9$;I#O)f1z-kJQ}zW@%^^N5utUZx)92~>;{HrNYTs!R3!u<2NEQOi#qn{C>Alk zVzG5(uux`tuEH(&r?s7!Wbl^V9pXWJ&xF%S^(lm_ zGQP#XvvEv2Y-8FqjeBOzUiFW=x=p$Yr(YfGuB-{}ku`?^Kk5J;Pa{Wn0e&w8!B<=K z8}i{=f9N#Vy|d1`TU07(Ww3M{#kpv0r-#HQ!r+8=YEPKRc@kjjKdb&?Ls zWY6ek=Nou`vpHRQmYC+ZBnSU)c9dbNoo?HuUiD|@9MzXzsIZ@3b=C!gy61M?XiwdE zPu-+hcT}OTVZ|SdEXhw1?f86}lJ-@pA~} zOqdGuEe?zAZ`cu|s`1@@2hy-qRmxXkI+z<7UKBvI!e0BJL;t7+)pKNW<=Pub+k0cw zf4sw@^L9m#Z`^(`Bj%pZ>Hy#k+@cXvT%n!Da@ik4C%0Z#hg8O6`$m>zO@#s@K?8z- zW-=0oSP6Qzcv$Q^#Z(en<%|~ig_WMGjLcwhnOC~90=9%XKR@r;q11}paeNJ3?zp^4 zujo>K(tp4F4^T@31QY-O00;mvPgqnNhrv<-hrv<;hrv<7nd!K1TX=|mtKto gI|IoE1ef6k1QeIBjRYzI-?5`eS;B}WZ!gGW~g{Z<}qy^<9vJ_i$UtP7Dz0&JtdEI0>zJHutObFBrr;DNq{Rg(CrbRJ%4nIAE?F^FL=mPt~P^T>~AS+n_0A= zx$0(6;7_|4~FeeyrF1@s7?NU;UzJZDU? zJLP{(p&Svmiu-`B8i1gM@i>9sC{J>_vb`eoXU$G?Y25&u(=t`tRo7~hVx$r>&uF?` zU4MJO%h&K{uG(V4Sp^|*V{Eh?GEx>Sh7zO2 zDr!NvzyQti+EE|dr2>J3R43S~s-+@HS(}1Rt;UI;lYiIbrpv;VLa;KE+fe2@O^~pL zt4O9|*qYEp0qAZ5N9(^O2w-M`!KC@29CX*h_f@vRU!MF#uVD_a9y*G5Z45gAcu<>9*$A+6w zrtud8>ETZs$ml6i?^{{;He!!Wy;%=xXWtd|VsMgwU$@tV=YHntz;g2p+C4!lm#L(& zKCFjS=}9-;*iAdZYnfXUJ>T>C@PG3!9{OzTJraoR6N$>HD3Hv`ldL{}<}6Zfs0Sy7ljsm;Y!L%*%>PRj>!!tv&Xdl# z*CqJ9w3q}BOpEE`2J^1U{h>Te_@9YMf47*Vc*dRzUKifC$7>3)TE8I)YCy&_I^kMH<&VS{TLCwGT_Nc{la|17fpG>qIe_hP3@Mj0JXSfcBE7L6& z&ww2cSSsbR8?SLEVB@$n} zDopI4+)lhI6wMx6)j=@pd2R5-G~Gh4NLKesDe)(LF9iMI;T+3;+)8}!Tj1t-U7145 zwD+FjaguVnf!aA$-7IL|w$~53H5>HF<&S$M(^dDA&75HVs?wR$!e2nTmYZw(G zLFmHfIJT1<;bv0T46EzM4AMG9tOvd`5}0{LFK}?E(bi?Murh9QJxz1-oThTESDtZ~ z6PXk3R+w-Jk~tL#J11}4WvO86)|^;xfHh-LV&WmuywUCe$8qVZ4u2)=&F-`!6l)~% zKoS>Aa>g7UR${&Ik!MTjohuo6y5tiScV%%o8Z;rTUn&i^DFRC=AlG|Y3Pp;DrVMX^ zt1h7av%mv(*c7%Qq@{s|E$p(?-Z-vb6(FdFPiKM6C)!!pS+KLPz}r)F!$aVT(EY7YD6JF8f?(H#OM*2z_-}6#S|;oU z^6hWDQCnA|X6B;BKzj(s?K2Cbr6m`Jvt<^t9N4VqSQh4@zJFLs;nKz0jkp830AnV` z8?ZYqT#C~oiZhp9SmRchfglJ1EsO}qV=qg7hJ?oo0}xmtVq|6|(z8xT7AI+YwPRQ< z&f`1xQz?0EUQqPHGPbEIT;cIX3Jfx9yJzk#9(ed4{?Om3^<)s+~UHK<=DkI_vH&rd%hG2<|@N8YyfSF`iEtL;St1YkkO3_ ztzcq^&}Ph&o>&P3?>NTo!l0{g)K$3U-z;2vJbzn9i)q)u7hQ$3s&L0O`QAak={}|h z{n5;kn7&6&eYbxD?Ve@aTH5=1_k1?>8g2&6=d`vHlMLRnyF)yP@0oBqsXm2pRmQjY z_co4chiy!ort!e6*{l9>SGP%5;q_gI2Wz$^pMzu990TNuG}3{`(qB&>CkK!Qkn3mPSQb{>>1tc zd;{+vHm6I^64U&a$oOIvGGb~k=+f|qj=IVtP1u&|x_dDphKWIVq9GP6X_D0h7CK%Q2uz%>h zM$zLN{~pYUxzMvZ0C)qpXv7p(Xs5BH^~cc3t=H8dXYqW#ktJDEp}-o@QXq1f)WacG zf}SlN7W+;ym4sF~qXm9SrRORmGgw^am9DISEn&{j&wI8awIX*sTtk;TzOB+L`i>v; z-%tMsP)h>@6aWAK2mmopSX3LFCWpvU0f)#^0*A;_1GmUh1cEpXZ-Yly2<1L_WeWfR zmzP(I1T_vY#T95O`Pl(5#T95O`InlD1T+D%m(Pm?IRQ4835*0g1Al}8m!X6K7MER& z1S$armxzo6NC8Nf(ToI00d1Umu5m!FLUKmpp9;Ee=I2K|Zz0001V Cu;mc| From ae219c16d6763823285d7c5dabaa1a1e78ca8726 Mon Sep 17 00:00:00 2001 From: kenzo44 Date: Fri, 13 Oct 2023 18:23:04 -0600 Subject: [PATCH 17/22] Moved bolus result to top --- ui/src/main/res/layout/dialog_wizard.xml | 74 ++++++++++++------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/ui/src/main/res/layout/dialog_wizard.xml b/ui/src/main/res/layout/dialog_wizard.xml index 6da6b7c2b3..885d3204f9 100644 --- a/ui/src/main/res/layout/dialog_wizard.xml +++ b/ui/src/main/res/layout/dialog_wizard.xml @@ -38,6 +38,43 @@ + + + + + + + + - - - - - - - - From 57afaafa5b0affb768bee471b7c37ab1758c3c70 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 14 Oct 2023 12:58:58 +0200 Subject: [PATCH 18/22] set full flavor default --- app/build.gradle | 1 + core/main/android_module_dependencies.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index e843863b82..401db3a2b4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -117,6 +117,7 @@ android { flavorDimensions = ["standard"] productFlavors { full { + getIsDefault().set(true) applicationId "info.nightscout.androidaps" dimension "standard" resValue "string", "app_name", "AAPS" diff --git a/core/main/android_module_dependencies.gradle b/core/main/android_module_dependencies.gradle index d8717e8cae..8ccf62d869 100644 --- a/core/main/android_module_dependencies.gradle +++ b/core/main/android_module_dependencies.gradle @@ -3,6 +3,7 @@ android { flavorDimensions = ["standard"] productFlavors { full { + getIsDefault().set(true) dimension "standard" } pumpcontrol { From eb9fa1a25b5886f3dca87200919c419953857e56 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 14 Oct 2023 13:53:28 +0200 Subject: [PATCH 19/22] NSC: optimize devicestatus precessing --- .../aaps/implementations/UiInteractionImpl.kt | 5 ++++ .../aaps/core/interfaces/ui/UiInteraction.kt | 1 + .../nsclient/data/NSDeviceStatusHandler.kt | 24 ++++++++++++++----- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt b/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt index 774c88d9c0..5672ea7650 100644 --- a/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt +++ b/app/src/main/kotlin/app/aaps/implementations/UiInteractionImpl.kt @@ -14,6 +14,7 @@ import app.aaps.activities.PreferencesActivity import app.aaps.core.interfaces.notifications.Notification import app.aaps.core.interfaces.nsclient.NSAlarm import app.aaps.core.interfaces.rx.bus.RxBus +import app.aaps.core.interfaces.rx.events.EventDismissNotification import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.main.events.EventNewNotification import app.aaps.core.ui.toast.ToastUtils @@ -169,6 +170,10 @@ class UiInteractionImpl @Inject constructor( } } + override fun dismissNotification(id: Int) { + rxBus.send(EventDismissNotification(id)) + } + override fun addNotification(id: Int, text: String, level: Int) { rxBus.send(EventNewNotification(Notification(id, text, level))) } diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt index f19feb9894..e56d83adce 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/ui/UiInteraction.kt @@ -68,6 +68,7 @@ interface UiInteraction { fun runCareDialog(fragmentManager: FragmentManager, options: EventType, @StringRes event: Int) + fun dismissNotification(id: Int) fun addNotification(id: Int, text: String, level: Int) fun addNotificationValidFor(id: Int, text: String, level: Int, validMinutes: Int) fun addNotificationWithSound(id: Int, text: String, level: Int, @RawRes soundId: Int?) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/data/NSDeviceStatusHandler.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/data/NSDeviceStatusHandler.kt index 9488c200ad..078faf90ac 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/data/NSDeviceStatusHandler.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclient/data/NSDeviceStatusHandler.kt @@ -2,13 +2,18 @@ package app.aaps.plugins.sync.nsclient.data import app.aaps.annotations.OpenForTesting import app.aaps.core.interfaces.configuration.Config +import app.aaps.core.interfaces.notifications.Notification import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData +import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.sharedPreferences.SP +import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.interfaces.utils.T import app.aaps.core.nssdk.interfaces.RunningConfiguration import app.aaps.core.nssdk.localmodel.devicestatus.NSDeviceStatus import app.aaps.core.utils.HtmlHelper import app.aaps.core.utils.JsonHelper +import app.aaps.plugins.sync.R import javax.inject.Inject import javax.inject.Singleton @@ -72,24 +77,31 @@ class NSDeviceStatusHandler @Inject constructor( private val config: Config, private val dateUtil: DateUtil, private val runningConfiguration: RunningConfiguration, - private val processedDeviceStatusData: ProcessedDeviceStatusData + private val processedDeviceStatusData: ProcessedDeviceStatusData, + private val uiInteraction: UiInteraction, + private val rh: ResourceHelper ) { fun handleNewData(deviceStatuses: Array) { var configurationDetected = false for (i in deviceStatuses.size - 1 downTo 0) { val nsDeviceStatus = deviceStatuses[i] - updatePumpData(nsDeviceStatus) - updateDeviceData(nsDeviceStatus) - updateOpenApsData(nsDeviceStatus) - updateUploaderData(nsDeviceStatus) - nsDeviceStatus.pump?.let { sp.putBoolean(app.aaps.core.utils.R.string.key_objectives_pump_status_is_available_in_ns, true) } // Objective 0 + if (config.NSCLIENT) { + updatePumpData(nsDeviceStatus) + updateDeviceData(nsDeviceStatus) + updateOpenApsData(nsDeviceStatus) + updateUploaderData(nsDeviceStatus) + } if (config.NSCLIENT && !configurationDetected) nsDeviceStatus.configuration?.let { // copy configuration of Insulin and Sensitivity from main AAPS runningConfiguration.apply(it) configurationDetected = true // pick only newest + } + if (config.APS) { + nsDeviceStatus.pump?.let { sp.putBoolean(app.aaps.core.utils.R.string.key_objectives_pump_status_is_available_in_ns, true) } // Objective 0 + } } } From ad375f4c446012e2c5427ea8987e06cbb0e4b86a Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 14 Oct 2023 19:26:23 +0200 Subject: [PATCH 20/22] New Crowdin updates (#2908) * New translations strings.xml (Romanian) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations oh_strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Dutch) * New translations strings.xml (Norwegian Bokmal) * New translations exam.xml (Norwegian Bokmal) * New translations objectives.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations exam.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations exam.xml (Norwegian Bokmal) * New translations objectives.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Russian) --- .../src/main/res/values-nb-rNO/strings.xml | 4 +-- .../ui/src/main/res/values-nb-rNO/strings.xml | 16 ++++++------ .../src/main/res/values-nb-rNO/strings.xml | 10 +++---- .../src/main/res/values-nb-rNO/strings.xml | 2 +- .../src/main/res/values-nb-rNO/strings.xml | 6 ++--- .../src/main/res/values-nb-rNO/exam.xml | 8 +++--- .../src/main/res/values-nb-rNO/objectives.xml | 2 +- .../src/main/res/values-nb-rNO/strings.xml | 26 +++++++++---------- .../src/main/res/values-nb-rNO/strings.xml | 6 ++--- .../src/main/res/values-nb-rNO/oh_strings.xml | 2 +- .../src/main/res/values-nb-rNO/strings.xml | 6 ++--- .../src/main/res/values-nb-rNO/strings.xml | 4 +-- .../src/main/res/values-nb-rNO/strings.xml | 2 +- .../src/main/res/values-nb-rNO/strings.xml | 4 +-- .../src/main/res/values-nb-rNO/strings.xml | 8 +++--- .../src/main/res/values-nb-rNO/strings.xml | 4 +-- .../src/main/res/values-nl-rNL/strings.xml | 1 + .../src/main/res/values-ro-rRO/strings.xml | 3 +++ .../src/main/res/values-ru-rRU/strings.xml | 6 +++++ .../src/main/res/values-nb-rNO/strings.xml | 2 +- ui/src/main/res/values-nb-rNO/strings.xml | 2 +- wear/src/main/res/values-nb-rNO/strings.xml | 4 +-- 22 files changed, 69 insertions(+), 59 deletions(-) diff --git a/core/interfaces/src/main/res/values-nb-rNO/strings.xml b/core/interfaces/src/main/res/values-nb-rNO/strings.xml index f0d8eeb0e6..4f650e3bf1 100644 --- a/core/interfaces/src/main/res/values-nb-rNO/strings.xml +++ b/core/interfaces/src/main/res/values-nb-rNO/strings.xml @@ -51,7 +51,7 @@ Vis Gj. snitt Delta Vis telefonbatteri Vis riggens batteri - Vis basalrate + Vis basaldose Vis loop status Vis BS Vis BGI @@ -75,7 +75,7 @@ Gj.snitt BS-endring (15min) Telefonbatteri (%) Rig-batteri (%) - Basalrate + Basaldose BGI verdi Tid (TT:MM eller TT:MM:SS) Time (TT) diff --git a/core/ui/src/main/res/values-nb-rNO/strings.xml b/core/ui/src/main/res/values-nb-rNO/strings.xml index e2d717ae04..af06f65e9f 100644 --- a/core/ui/src/main/res/values-nb-rNO/strings.xml +++ b/core/ui/src/main/res/values-nb-rNO/strings.xml @@ -75,7 +75,7 @@ Insulinets virkningstid (DIA) Insulin-karbohydratfaktor (IK) Insulin sensitivitetsfaktor (ISF) - Basalrate + Basaldose Blodsukkermål g % @@ -193,7 +193,7 @@ %1$d min - Careportal + Helseportal BS-kontroll Manuelt BS eller kalibrering Melding @@ -294,19 +294,19 @@ AVBRYT BOLUS AVBRYT FORLENGET BOLUS AVBRYT MIDL. MÅL - CAREPORTAL + HELSEPORTAL BYTTE SLANGESETT BYTTE RESERVOAR KALIBRERING PRIME BOLUS BEHANDLING - CAREPORTAL NS OPPDATERING + HELSEPORTAL NS-OPPDATERING PROFILBYTTE NS OPPDATERING BEHANDLINGER NS OPPDATERING OPPDATER MIDL. MÅL NS AUTOMASJON FJERNET BS FJERNET - CAREPORTAL FJERNET + HELSEPORTAL FJERNET BOLUS FJERNET KARBO FJERNET MIDL. MÅL FJERNET @@ -486,7 +486,7 @@ Insulinresistent voksen Graviditet Velg pasienttype for oppsett av sikkerhetsgrenser - Maks tillatt bolus [U] + Maks tillatt bolus [E] Maks tillatt karbohydrater [g] Pasienttype @@ -561,9 +561,9 @@ Mangler SMS-tillatelse - Hvordan hindre at appen stenges? + Ikke steng appen min? Opplast av krasjlogger er deaktivert! - \n\nDokumentasjon:\nhttps://androidaps.readthedocs.io\n\nfacebook:\nhttps://www.facebook.com/groups/AndroidAPSUsers + \n\nDokumentasjon:\nhttps://androidaps.readthedocs.io\n\nFacebook:\nhttps://www.facebook.com/groups/AndroidAPSUsers %1$d dag %1$d dager diff --git a/plugins/aps/src/main/res/values-nb-rNO/strings.xml b/plugins/aps/src/main/res/values-nb-rNO/strings.xml index c66aa421a2..e508c95944 100644 --- a/plugins/aps/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/aps/src/main/res/values-nb-rNO/strings.xml @@ -41,8 +41,8 @@ Bruk Autosens-funksjon Max E/t en midl. basal kan settes til Denne verdien kalles max basal i OpenAPS - Maksimum basal IOB som OpenAPS kan levere [U] - Denne verdien kalles Max IOB i OpenAPS.\nDet er maks insulinmengde i [U] som APS kan levere i en dose. + Maksimum basal IOB som OpenAPS kan levere [E] + Denne verdien kalles Max IOB i OpenAPS.\nDet er maks insulinmengde i [E] som APS kan levere i en dose. Standard verdi: sann\nGir autosens tillatelse til å justere BS-mål, i tillegg til ISF og basaler. Autosens justerer også BS målverdier Standardverdi er: 3.0 (AMA) eller 8.0 (SMB). Dette er grunninnstillingen for KH-opptak per 5 minutt. Den påvirker hvor raskt COB skal reduseres, og benyttes i beregning av fremtidig BS-kurve når BS enten synker eller øker mer enn forventet. Standardverdi er 3mg/dl/5 min. @@ -54,8 +54,8 @@ Nyttig når data fra ufiltrerte kilder som xDrip+ registrerer mye støy. Multiplikator for maks daglig basal Multiplikator for gjeldende basal - Maks total IOB OpenAPS ikke kan overstige [U] - Denne verdien kalles Maks IOB av OpenAPS\nOpenAPS vil ikke gi mere insulin hvis mengden insulin ombord (IOB) overstiger denne verdien + Maks total IOB OpenAPS ikke kan overstige [E] + Denne verdien kalles Maks IOB av OpenAPS\nAAPS vil ikke gi mere insulin hvis mengden insulin ombord (IOB) overstiger denne verdien Aktiver UAM Aktiver SMB Bruk Supermikrobolus i stedet for midl. basal for raskere resultat @@ -65,7 +65,7 @@ Aktiver SMB etter karbohydrater Aktiver SMB i 6t etter karbohydratinntak, selv med 0 COB. Bare mulig med en bra filtrert BS kilde som f. eks. Dexcom G5/G6 Aktiver SMB med COB - Aktiver SMB når COB er aktiv. + Aktiver SMB når COB (karbohydrater ombord) er aktiv. Aktiver SMB med midl. målverdi Aktiver SMB når midl. målverdi er aktivert (spise snart, trening) Aktiver SMB ved høy midl. målverdi diff --git a/plugins/automation/src/main/res/values-nb-rNO/strings.xml b/plugins/automation/src/main/res/values-nb-rNO/strings.xml index 0b67c24fef..6a838824ae 100644 --- a/plugins/automation/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/automation/src/main/res/values-nb-rNO/strings.xml @@ -87,7 +87,7 @@ COB %1$s %2$.0f Puls HR %1$s %2$.0f - IOB [U]: + IOB [E]: Dist [m]: Gjentakende tidspunkt Hver diff --git a/plugins/configuration/src/main/res/values-nb-rNO/strings.xml b/plugins/configuration/src/main/res/values-nb-rNO/strings.xml index 3a00df1b2a..f231df03b5 100644 --- a/plugins/configuration/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/configuration/src/main/res/values-nb-rNO/strings.xml @@ -117,12 +117,12 @@ Databaseopprydding Vil du virkelig nullstille databasene? Vedlikeholdsinnstillinger - E-post mottaker + Mottaker av e-post Antall logger du vil sende Send logger via e-post Slett logger - Nightscout versjon: - Engineering Mode aktivert + Nightscout-versjon: + Engineering mode aktivert Loggfiler Logginnstillinger Annet diff --git a/plugins/constraints/src/main/res/values-nb-rNO/exam.xml b/plugins/constraints/src/main/res/values-nb-rNO/exam.xml index 8da0332af8..ee21a0a22c 100644 --- a/plugins/constraints/src/main/res/values-nb-rNO/exam.xml +++ b/plugins/constraints/src/main/res/values-nb-rNO/exam.xml @@ -167,13 +167,13 @@ For å registrere karbohydrater som brukes til å korrigere lavt blodsukker. https://wiki.aaps.app/en/latest/Usage/Extended-Carbs.html Fjernovervåking - Hvordan kan du overvåke AndroidAPS (for eksempel for ditt barn) på eksternt? - AAPSClient app, Nightscout app og Nightscout websiden gjør det mulig for deg å følge AAPS eksternt. + Hvordan kan du eksternt overvåke AndroidAPS (for eksempel for ditt barn)? + Appene AAPSClient og xDrip+ samt Nightscout-websiden gjør det mulig for deg å følge AAPS eksternt. Andre apper (f.eks. Dexcom follow, xDrip som kjører i følger modus) lar deg følge noen parametere (f.eks. blodsukker/sensor verdier) på avstand, men bruker forskjellige beregningsmetoder og kan derfor vise andre IOB eller COB verdier. For å følge AAPS eksternt må begge enhetene ha Internett-tilgang (f.eks. via Wi-Fi eller mobildata). AAPSClient som brukes som ekstern følger app vil både overvåke og gi full kontroll over AAPS. https://wiki.aaps.app/en/latest/Children/Children.html - Insulin Sensitivitetsfaktor (ISF) + Insulinsensitivitetsfaktor (ISF) Økte ISF-verdiene vil føre til levering av mer insulin for å dekke opp en viss mengde karbohydrater. Redusering av ISF verdien vil føre til mer insulintilførsel for å korrigere et blodsukker som ligger over målverdien. Å øke eller senke ISF verdien har ingen effekt på insulintilførselen når blodsukkeret er lavere enn målverdien. @@ -211,7 +211,7 @@ Gjør et profilbytte til mer enn 100%. https://wiki.aaps.app/en/latest/Usage/Profiles.html#timeshift Endring av profil - Basalrater, ISF, KH ratio, etc., bør defineres i profiler. + Basaldoser, ISF, IK-faktor osv. bør defineres i profiler. Aktivering av endringer i Nightscout profilen din krever at din AndroidAPS telefon er koblet til Internett. Å redigere verdier i profilen din er tilstrekkelig for å aktivere profilendringen. Flere profiler kan defineres og velges for å håndtere endringer i omstendigheter (f.eks. hormonelle endringer, skift arbeid, hverdager/helgedager). diff --git a/plugins/constraints/src/main/res/values-nb-rNO/objectives.xml b/plugins/constraints/src/main/res/values-nb-rNO/objectives.xml index 291f461957..1ab33145aa 100644 --- a/plugins/constraints/src/main/res/values-nb-rNO/objectives.xml +++ b/plugins/constraints/src/main/res/values-nb-rNO/objectives.xml @@ -24,7 +24,7 @@ 1 uke vellykket looping på dagtid hvor alle måltider (KH) angis Hvis dine autosens resultater ikke ligger rundt 100% kan det tyde på at profilen din er feil. Aktiver ekstra funksjoner for bruk på dagtid, slik som SMB (Super Micro Bolus) - Du må lese wiki og øke din maxIOB for å få SMB til å fungere. Et godt utgangspunkt er maxIOB = gjennomsnittlig måltidsbolus + 3*max daglig basal + Du må lese wiki og øke din maxIOB for å få SMB til å fungere. Et godt utgangspunkt er å sette maxIOB til gjennomsnittlig måltidsbolus + 3x max daglig basaldose Bruk av SMB er en målsetting. Oref1 algoritmen ble designet for å hjelpe deg med dine bolusdoseringer. Det anbefales å ikke gi full bolusdose for måltider, men bare en del av den, og la AAPS styre resten om nødvendig. På denne måten får du større fleksibilitet med hensyn på feilberegnede KH. Visste du at du kan angi en prosentandel for boluskalkulatoren som resulterer i redusert bolusstørrelse? Aktivere ekstra funksjoner for bruk på dagtid, slik som Dynamisk Sensitivitet Sørg for at SMB fungerer som den skal. Aktiver DynamicISF-tillegget og finn riktig kalibrering for dine behov. Det anbefales å starte med en verdi lavere enn 100% for sikkerhets skyld. diff --git a/plugins/main/src/main/res/values-nb-rNO/strings.xml b/plugins/main/src/main/res/values-nb-rNO/strings.xml index 6632e8430a..8fce83a7cb 100644 --- a/plugins/main/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/main/src/main/res/values-nb-rNO/strings.xml @@ -156,7 +156,7 @@ Handlinger Hurtigknapper for rask tilgang til ofte brukte funksjoner - ACT + HAN Midlertidig basal Forlenget bolus Avbryt forlenget bolus @@ -174,18 +174,18 @@ Pumpe Vis statusindikatorer på hjem-skjermen - Terskel for advarsel om alder på slangesett [h] - Terskel for kritisk alder på slangesett [h] - Terskel for advarsel, alder på insulin [h] - Terskel for kritisk alder på insulin [h] - Terskel for advarsel, alder på CGM [h] - Terskel for kritisk alder på CGM [h] + Terskel for advarsel om alder på slangesett [t] + Terskel for kritisk alder på slangesett [t] + Terskel for advarsel, alder på insulin [t] + Terskel for kritisk alder på insulin [t] + Terskel for advarsel, alder på sensor [t] + Terskel for kritisk alder på sensor [t] Terskel for advarsel, batterinivå for sensor [%] Terskel for kritisk batterinivå for sensor [%] - Terskel for advarsel, batterialder for pumpe [h] - Terskel for kritisk batterialder for pumpe [h] - Terskel for advarsel, insulinreservoar [U] - Terskel for kritisk insulinreservoar [U] + Terskel for advarsel, batterialder for pumpe [t] + Terskel for kritisk batterialder for pumpe [t] + Terskel for advarsel, insulinreservoar [E] + Terskel for kritisk insulinreservoar [E] Terskel for advarsel, batterinivå for pumpe [%] Terskel for kritisk batterinivå for pumpe [%] Kopier innstillingene fra NS @@ -242,11 +242,11 @@ Høy verdi Korte navn i menyfaner Vis merknadsfelt i dialogvindu for boluskalkulator - Bolusveiviser utfører beregninger, men bare denne del av beregnet insulin leveres. Nyttig ved bruk av SMB-algoritmen. + Boluskalkulator utfører beregninger, men bare denne del av beregnet insulin leveres. Nyttig ved bruk av SMB-algoritmen. Gi full bolus (100 %) dersom blodsukker er eldre enn Aktiver bolusveileder Bruk en påminnelse om å spise senere istedet for boluskalkulatorens resultat når blodsukker er høyt (\"pre-bolus\") - Aktiver superbolus i veiviser + Aktiver superbolus i boluskalkulator Aktiver superbolus-funksjonen i boluskalkulatoren. Ikke aktiver denne før du vet hvordan den fungerer. DEN KAN LEDE TIL EN OVERDOSERING AV INSULIN HVIS DEN BRUKES UKRITISK! Aktiver boluspåminnelse Bruk en påminnelse for å sette bolusdosen senere med boluskalkulatoren («post bolus») diff --git a/plugins/sensitivity/src/main/res/values-nb-rNO/strings.xml b/plugins/sensitivity/src/main/res/values-nb-rNO/strings.xml index 623b93010d..0d1a525bf3 100644 --- a/plugins/sensitivity/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/sensitivity/src/main/res/values-nb-rNO/strings.xml @@ -9,15 +9,15 @@ Sensitivitet beregnes som en vektet gjennomsnittsverdi av avvikene. Ferske avvik har høyere vekting. Minimum opptak av karbohydrater beregnes ut fra maks opptakstid for karbohydrater angitt i dine innstillinger. Denne algoritmen er den raskeste for å justere endringer i sensitivitet. UAM deaktivert fordi den trenger Oref1 sensitivitetsplugin Absorberingsinnstillinger - Maks absorberingstid for måltid [h] + Maks absorberingstid for måltid [t] Tid i timer hvor det forventes at alle karbohydrater fra måltid vil være absorbert - Intervall for autosens [h] + Intervall for autosens [t] Antall timer med historiske data for beregning av sensitivitet (absorpsjonstid for KH er ekskludert) Standardverdi: 1.2\nDette er en multiplikatorbegrensning for autosens (og snart autotune) som begrenser at autosens ikke kan øke med mer enn 20%%, som dermed begrenser hvor mye autosens kan justere opp dine basaler, hvor mye ISF kan reduseres og hvor lavt BS målverdi kan settes. Standardverdi: 0.7\nDette er en multiplikatorbegrensning for autosens-sikkerhet. Den begrenser autosens til å redusere basalverdier, og øke isulinssensitivitet (ISF) og BS mål med ikke mer enn enn 30%. Maks autosens ratio Minimum autosens ratio Standardverdi er: 3.0 (AMA) eller 8.0 (SMB). Dette er grunninnstillingen for KH-opptak per 5 minutt. Den påvirker hvor raskt COB skal reduseres, og benyttes i beregning av fremtidig BS-kurve når BS enten synker eller øker mer enn forventet. Standardverdi er 3mg/dl/5 min. - Maks absorpsjonstid for måltid [h] + Maks absorpsjonstid for måltid [t] Etter denne tiden forventes det at måltidet er absorbert. Eventuelle gjenværende karbo vil tas ut av beregninger. diff --git a/plugins/sync/src/main/res/values-nb-rNO/oh_strings.xml b/plugins/sync/src/main/res/values-nb-rNO/oh_strings.xml index c33f512c0d..4ec943494a 100644 --- a/plugins/sync/src/main/res/values-nb-rNO/oh_strings.xml +++ b/plugins/sync/src/main/res/values-nb-rNO/oh_strings.xml @@ -27,7 +27,7 @@ Boluser Forlenget bolus Karbohydrater - Careportal hendelser (unntatt notater) + Helseportal-hendelser (unntatt notater) Profilbytter Totale daglige doser Midlertidige basal doser diff --git a/plugins/sync/src/main/res/values-nb-rNO/strings.xml b/plugins/sync/src/main/res/values-nb-rNO/strings.xml index e91f3141de..dbed92b638 100644 --- a/plugins/sync/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/sync/src/main/res/values-nb-rNO/strings.xml @@ -11,7 +11,7 @@ Tillat tilkobling i roaming Lag meldinger ved feil Opprett varslinger hvis det er nødvendig med karbohydrater - Opprett varslinger i Nightscout ved feil eller meldinger (også synlig i Careportal under Behandlinger) + Opprett varslinger i Nightscout ved feil eller meldinger (også synlig i Helseportal under Behandlinger) Opprett Nightscout-meldinger ved behov for karbohydrater Synkroniserer dine data med Nightscout v1 API Synkroniserer dine data med Nightscout v3 API @@ -54,11 +54,11 @@ Motta midlertidige mål Aksepter midlertidige mål angitt med NS eller AAPSClient Motta profilbytter - Aksepter profilbytter som er angitt via NS eller NSClient + Aksepter profilbytter som er angitt via NS eller AAPSClient Motta APS offline hendelser Aksepter APS offline hendelser lagt inn gjennom NS eller AAPSClient Motta TBR og EB - Godta TBR og EB beregninger fra tilleggsmodul + Godta midlertidig basal og forlenget bolus lagt inn fra en annen instans Motta insulin Aksepter insulin angitt via NS eller AAPSClient (enhetene er ikke dosert, kun beregnet mot IOB) Motta karbohydrater diff --git a/pump/combo/src/main/res/values-nb-rNO/strings.xml b/pump/combo/src/main/res/values-nb-rNO/strings.xml index 1883de382d..b13cd45cd5 100644 --- a/pump/combo/src/main/res/values-nb-rNO/strings.xml +++ b/pump/combo/src/main/res/values-nb-rNO/strings.xml @@ -36,13 +36,13 @@ Ugyldig oppsett av pumpen. Les dokumentasjonen og sjekk at Quick Info menyen heter QUICK INFO ved hjelp av 360 programvaren. Leser basalprofil Pumpe historikken har blitt endret siden bolus kalkuleringen ble utført. Bolus har ikke blitt levert. Vennligst rekalkuler om bolus fortsatt er nødvendig. - Bolus har blitt levert, men det oppsto en feil ved loggføring i behandlinger. Dette kan oppstå hvis to små bolus på samme størrelse blir levert i løpet av to minutter. Vennligst sjekk pumpe historikken og behandlinger loggen, og bruk Careportal for å legge til de manglende behandlingene. Pass på at du ikke legger til to identiske behandlinger på samme minutt. + Bolus har blitt levert, men det oppsto en feil ved loggføring i behandlinger. Dette kan oppstå hvis to små bolus på samme størrelse blir levert i løpet av to minutter. Vennligst sjekk pumpe historikken og behandlinger loggen, og bruk Helseportal for å legge til de manglende behandlingene. Pass på at du ikke legger til to identiske behandlinger på samme minutt. Avviser høy temp target siden kalkuleringen ikke tok hensyn til nylige endringer i pumpe historikken Oppdaterer pumpestatus Basal dosen i pumpen har blitt endret og vil i løpet av kort tid bli oppdatert Basalsats endret i pumpe, men lesing av den feilet Sjekker for endringer i historikken - Flere boluser levert i samme minutt og med samme insulinmengde ble importert. Bare en av doseringene ble lagt til i behandlinger. Vennligst sjekk pumpen og legg manuelt til ekstra bolus doseringer i Careportal. Ikke legg til flere boluser i samme minutt. + Flere boluser levert i samme minutt og med samme insulinmengde ble importert. Bare en av doseringene ble lagt til i behandlinger. Vennligst sjekk pumpen og legg manuelt til ekstra bolus doseringer i Helseportal. Ikke legg til flere boluser i samme minutt. Den siste bolus er eldre enn 24t eller er i fremtiden. Vennligst sjekk at datoen i pumpen er korrekt. Tid/dato for levert bolus i pumpen er trolig feil, og IOB beregningen blir da feil. Vennligst sjekk pumpens tid/dato. Antall boluser diff --git a/pump/combov2/src/main/res/values-nb-rNO/strings.xml b/pump/combov2/src/main/res/values-nb-rNO/strings.xml index c31eb9b14f..e905e4cfb5 100644 --- a/pump/combov2/src/main/res/values-nb-rNO/strings.xml +++ b/pump/combov2/src/main/res/values-nb-rNO/strings.xml @@ -100,7 +100,7 @@ knappene samtidig for å avbryte parringen)\n Lar aktive emulert 100% TBR få avslutte Ignorerer redundant 100% TBR forespørsel Uventet begrensning oppsto ved justering av TBR: målprosenten var %1$d%%, nådde grense på %2$d%% - Kan ikke sette absolutt TBR hvis basalraten er null + Kan ikke sette absolutt TBR hvis basaldosen er null Sammenkoble AndroidAPS og Android med en ikke-tilkoblet Accu-Chek Combo pumpe Koble fra AndroidAPS og Android fra den ilkoblede Accu-Chek Combo pumpen Ukjent TBR ble oppdaget og stoppet; prosent: %1$d%%, gjenværende varighet: %2$s diff --git a/pump/dana/src/main/res/values-nb-rNO/strings.xml b/pump/dana/src/main/res/values-nb-rNO/strings.xml index 211c2198e2..e561897b03 100644 --- a/pump/dana/src/main/res/values-nb-rNO/strings.xml +++ b/pump/dana/src/main/res/values-nb-rNO/strings.xml @@ -107,9 +107,9 @@ Bolus hastighet Valgt pumpe Logg reservoar bytte - Legg til \"Insulinbytte\" i Careportal når den oppdages i historikken + Legg til \"Insulinbytte\" i Helseportal når den oppdages i historikken Logg kanyle bytte - Legg til \"Kanylebytte\" i Careportal når den oppdages i historikken + Legg til \"Kanylebytte\" i Helseportal når den oppdages i historikken PIN1 PIN2 Trykk OK på pumpen\nog skriv inn de 2 viste tallene\nHold skjermen på pumpen PÅ ved å trykke minus knappen til du fullfører inntastingen. diff --git a/pump/diaconn/src/main/res/values-nb-rNO/strings.xml b/pump/diaconn/src/main/res/values-nb-rNO/strings.xml index 23b32d57b3..744ca26447 100644 --- a/pump/diaconn/src/main/res/values-nb-rNO/strings.xml +++ b/pump/diaconn/src/main/res/values-nb-rNO/strings.xml @@ -85,10 +85,10 @@ aps_incarnation_no pump_serial_no Logg reservoar bytte - Legg til \"Insulinbytte\" i Careportal når den oppdages i historikken + Legg til \"Insulinbytte\" i Helseportal når den oppdages i historikken Logg bytte av kanyle - Legg til \"Bytte av injeksjonssted\" i Careportal når den oppdages i historikken - Legg til \"Bytte av batteri\" i Careportal når den oppdages i historikken + Legg til \"Bytte av injeksjonssted\" i Helseportal når den oppdages i historikken + Legg til \"Bytte av batteri\" i Helseportal når den oppdages i historikken Logg batteri bytte Logg synkronisering pågår Lavt insulinnivå @@ -145,7 +145,7 @@ Når basal oppsett er fullført, kan basaldoseringer startes. Kommandoen ble ikke utført. Vennligst prøv igjen. Logg bytte av slangesett - Legg til \"Slangesettbytte\" i Careportal når den oppdages i historikken + Legg til \"Slangesettbytte\" i Helseportal når den oppdages i historikken Temp Basal startet Vel Lav Glukose Stopp (LGS) er injeksjoner begrenset LGS status er PÅ. PÅ kommando er nektet. diff --git a/pump/medtrum/src/main/res/values-nb-rNO/strings.xml b/pump/medtrum/src/main/res/values-nb-rNO/strings.xml index c72e66c4d5..8100bc66f2 100644 --- a/pump/medtrum/src/main/res/values-nb-rNO/strings.xml +++ b/pump/medtrum/src/main/res/values-nb-rNO/strings.xml @@ -21,7 +21,7 @@ %.2f E %.2f V Basal type - Basalrate + Basaldose %.2f E/t Pumpetype FW versjon @@ -85,7 +85,7 @@ Trykk Neste for å fortsette. Trykk Neste for å starte aktivering. Fjern sikkerhetslåsen. Koble pumpen til kroppen. Trykk nål-knappen. - Aktiverer pumpe og setter basalrate. Vennligst vent. + Aktiverer pumpe og setter basaldose. Vennligst vent. Kunne ikke aktivere, trykk Prøv igjen for å prøve igjen. Ny patch aktivert. %.2f enheter gjenstår. Trykk OK for å gå tilbake til hovedskjermen. diff --git a/pump/medtrum/src/main/res/values-nl-rNL/strings.xml b/pump/medtrum/src/main/res/values-nl-rNL/strings.xml index 1695ea051f..c566589058 100644 --- a/pump/medtrum/src/main/res/values-nl-rNL/strings.xml +++ b/pump/medtrum/src/main/res/values-nl-rNL/strings.xml @@ -53,6 +53,7 @@ Batterij leeg Geen kalibratie Update van pomp tijdzone mislukt, snooze bericht en vernieuw handmatig. + Bolus fout Opnieuw Volgende diff --git a/pump/medtrum/src/main/res/values-ro-rRO/strings.xml b/pump/medtrum/src/main/res/values-ro-rRO/strings.xml index 1b55a6cc25..01b5297304 100644 --- a/pump/medtrum/src/main/res/values-ro-rRO/strings.xml +++ b/pump/medtrum/src/main/res/values-ro-rRO/strings.xml @@ -53,6 +53,7 @@ Baterie descărcată Fără calibrare Actualizarea fusului orar al pompei a eșuat, amână mesajul și actualizează manual. + Eroare bolus Încearcă din nou Înainte @@ -101,6 +102,8 @@ Apasă Înainte pentru a relua activarea sau Renunțare pentru a reseta statusul activării. Te rog, așteaptă. Se citește starea de activare din pompă. + Alertă de inaccesibilitate activată forțat, deoarece patchul Medtrum poate eșua și să nu fie accesibil. + Recomandat să se seteze la 30 de minute, deoarece patchul Medtrum poate eșua și să nu fie accesibil. Număr de serie Introdu numărul de serie al bazei pompei. Număr de serie invalid! diff --git a/pump/medtrum/src/main/res/values-ru-rRU/strings.xml b/pump/medtrum/src/main/res/values-ru-rRU/strings.xml index 533755408c..c029351280 100644 --- a/pump/medtrum/src/main/res/values-ru-rRU/strings.xml +++ b/pump/medtrum/src/main/res/values-ru-rRU/strings.xml @@ -6,6 +6,7 @@ Интеграция с помпами Medtrum Nano и Medtrum 300U Настройки Medtrum Ошибка помпы: %1$s !! + Помпа - Предупреждение: %1$s Помпа приостановлена Помпа приостановлена из-за превышения максимального количества инсулина в час Помпа приостановлена в связи с превышением максимального допустимого количества инсулина в сутки @@ -52,6 +53,7 @@ Батарея разряжена Нет калибровки Не удалось обновить часовой пояс помпы, закройте сообщение и обновите вручную. + Болюс - ошибка Повторить Далее @@ -100,12 +102,16 @@ Нажмите Далее, чтобы возобновить активацию или Discard для сброса статуса активации. Пожалуйста, подождите, получение статуса активации с помпы. + Принудительно включена сигнализация о недоступности помпы, так как патч Medtrum может выйти из строя и быть недоступным. + Рекомендуется установить значение в 30 минут, так как патч Medtrum может выйти из строя и быть недоступным. Серийный номер Введите серийный номер основания вашей помпы. Неверный серийный номер! Помпа не тестировалась: %1$d! Свяжитесь с нами в discord или github Настройки оповещений Выберите предпочитаемые параметры оповещений помпы. + Уведомление об оповещениях помпы + Показывать уведомления о некритических оповещениях помпы: низком заряде батареи, низком запасе инсулина (20 единиц) и приближающемся истечении срока работы. Рекомендуется оставить включенным, когда звук оповещений выключен. Окончание срока действия патча Если включено, то патч закончит свое действие через 3 дня, с дополнительными 8 часами после этого. Максимальное количество инсулина в час diff --git a/pump/omnipod-dash/src/main/res/values-nb-rNO/strings.xml b/pump/omnipod-dash/src/main/res/values-nb-rNO/strings.xml index dffebca5c9..b7b4c84bed 100644 --- a/pump/omnipod-dash/src/main/res/values-nb-rNO/strings.xml +++ b/pump/omnipod-dash/src/main/res/values-nb-rNO/strings.xml @@ -43,7 +43,7 @@ Profilen ble angitt Pausing av insulintilførsel ble ikke bekreftet! Vennligst oppdater Pod-status fra Omnipod-fanen og gjenoppta insulinlevering om nødvendig. Insulintilførsel er pauset - Tidssone på pod er forskjellig fra tidssonen på telefon. Basalrater er feil. Bytt profil for å korrigere + Tidssone på pod er forskjellig fra tidssonen på telefon. Basaldoser er feil. Bytt profil for å korrigere Kunne ikke sette ny basalprofil. Insulintilførsel er pauset Endring av basalprofil kan ha feilet. Insulinlevering kan bli stoppet! Vennligst velg Oppdater Pod fra Omnipod-fanen og velg gjenoppta levering hvis nødvendig. Status for levering av bolusdoser er usikker. Oppdater pod-statusen for å verifisere. diff --git a/ui/src/main/res/values-nb-rNO/strings.xml b/ui/src/main/res/values-nb-rNO/strings.xml index 09efac2ec8..206e1310d4 100644 --- a/ui/src/main/res/values-nb-rNO/strings.xml +++ b/ui/src/main/res/values-nb-rNO/strings.xml @@ -124,7 +124,7 @@ DPV standardprofil Ugyldig % verdi - Basalrate + Basaldose STOPP er trykket diff --git a/wear/src/main/res/values-nb-rNO/strings.xml b/wear/src/main/res/values-nb-rNO/strings.xml index 426e56cc75..08c0405bf7 100644 --- a/wear/src/main/res/values-nb-rNO/strings.xml +++ b/wear/src/main/res/values-nb-rNO/strings.xml @@ -36,7 +36,7 @@ Vis Gj.snittDelta Vis telefonbatteri Vis riggens batteri - Vis basalrate + Vis basaldose Vis loop status Vis BS Vis BGI @@ -109,7 +109,7 @@ eKarbo Prosent Start [min] - Varighet [h] + Varighet [t] Insulin Forhåndsinnstilling 1 Forhåndsinnstilling 2 From b3081773a7f5485d59712d432963f64dcb5f7930 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 14 Oct 2023 20:10:46 +0200 Subject: [PATCH 21/22] fix tests --- .../nsclientV3/extensions/DeviceStatusExtensionKtTest.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/DeviceStatusExtensionKtTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/DeviceStatusExtensionKtTest.kt index f1cdf04ba3..1213d40efc 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/DeviceStatusExtensionKtTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/DeviceStatusExtensionKtTest.kt @@ -5,6 +5,7 @@ import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData import app.aaps.core.interfaces.objects.Instantiator import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.sharedPreferences.SP +import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.nssdk.interfaces.RunningConfiguration import app.aaps.core.nssdk.mapper.convertToRemoteAndBack @@ -16,6 +17,7 @@ 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 @Suppress("SpellCheckingInspection") internal class DeviceStatusExtensionKtTest : TestBase() { @@ -26,6 +28,7 @@ internal class DeviceStatusExtensionKtTest : TestBase() { @Mock lateinit var config: Config @Mock lateinit var runningConfiguration: RunningConfiguration @Mock lateinit var instantiator: Instantiator + @Mock lateinit var uiInteraction: UiInteraction private lateinit var processedDeviceStatusData: ProcessedDeviceStatusData private lateinit var nsDeviceStatusHandler: NSDeviceStatusHandler @@ -33,7 +36,8 @@ internal class DeviceStatusExtensionKtTest : TestBase() { @BeforeEach fun setup() { processedDeviceStatusData = ProcessedDeviceStatusDataImpl(rh, dateUtil, sp, instantiator) - nsDeviceStatusHandler = NSDeviceStatusHandler(sp, config, dateUtil, runningConfiguration, processedDeviceStatusData) + nsDeviceStatusHandler = NSDeviceStatusHandler(sp, config, dateUtil, runningConfiguration, processedDeviceStatusData, uiInteraction, rh) + Mockito.`when`(config.NSCLIENT).thenReturn(true) } @Test From 20e4590eea78d76fb628f88bcda2693a04b53b4d Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 14 Oct 2023 22:00:02 +0200 Subject: [PATCH 22/22] fix HR graph scale --- .../src/main/java/com/jjoe64/graphview/series/BaseSeries.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java b/core/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java index bd7cb5081a..d5af6a5449 100644 --- a/core/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java +++ b/core/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java @@ -140,7 +140,7 @@ public abstract class BaseSeries implements Series * @return the highest y value, or 0 if there is no data */ public double getHighestValueY() { - if (mData.isEmpty()) return 0d; + if (mData.isEmpty()) return 100d; double h = mData.get(0).getY(); for (int i = 1; i < mData.size(); i++) { double c = mData.get(i).getY();