diff --git a/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt b/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt index 530b990657..407c135495 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.sync.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..02ab7bd4c6 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.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 @@ -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/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/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/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()) } } 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..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 @@ -22,7 +22,7 @@ import dagger.android.ContributesAndroidInjector SkinsUiModule::class, ActionsModule::class, WearModule::class, - OverviewModule::class + OverviewModule::class, ] ) 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/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedList.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedList.kt new file mode 100644 index 0000000000..ab82262a2b --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedList.kt @@ -0,0 +1,187 @@ +package app.aaps.plugins.sync.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/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt new file mode 100644 index 0000000000..da1d4f084e --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -0,0 +1,245 @@ +package app.aaps.plugins.sync.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.sync.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.SYNC) + .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/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt new file mode 100644 index 0000000000..de327c909c --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt @@ -0,0 +1,257 @@ +package app.aaps.plugins.sync.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/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt new file mode 100644 index 0000000000..69f2f44a62 --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt @@ -0,0 +1,41 @@ +package app.aaps.plugins.sync.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/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt new file mode 100644 index 0000000000..e762d27b49 --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt @@ -0,0 +1,88 @@ +package app.aaps.plugins.sync.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/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/sync/src/main/res/xml/pref_garmin.xml b/plugins/sync/src/main/res/xml/pref_garmin.xml new file mode 100644 index 0000000000..1301693ec7 --- /dev/null +++ b/plugins/sync/src/main/res/xml/pref_garmin.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedListTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedListTest.kt new file mode 100644 index 0000000000..1902fdc4ec --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/DeltaVarEncodedListTest.kt @@ -0,0 +1,192 @@ +package app.aaps.plugins.sync.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/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt new file mode 100644 index 0000000000..c0af17edfa --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt @@ -0,0 +1,116 @@ +package app.aaps.plugins.sync.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/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt new file mode 100644 index 0000000000..d89dfa9156 --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt @@ -0,0 +1,99 @@ +package app.aaps.plugins.sync.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/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt new file mode 100644 index 0000000000..6f1cccad86 --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt @@ -0,0 +1,201 @@ +package app.aaps.plugins.sync.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