diff --git a/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt b/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt index 530b990657..28075f34f3 100644 --- a/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt +++ b/app/src/main/kotlin/app/aaps/activities/MyPreferenceFragment.kt @@ -43,7 +43,8 @@ import app.aaps.plugins.configuration.maintenance.MaintenancePlugin import app.aaps.plugins.constraints.safety.SafetyPlugin import app.aaps.plugins.insulin.InsulinOrefFreePeakPlugin import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin -import app.aaps.plugins.main.general.wear.WearPlugin +import app.aaps.plugins.sync.garmin.GarminPlugin +import app.aaps.plugins.sync.wear.WearPlugin import app.aaps.plugins.sensitivity.SensitivityAAPSPlugin import app.aaps.plugins.sensitivity.SensitivityOref1Plugin import app.aaps.plugins.sensitivity.SensitivityWeightedAveragePlugin @@ -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..6ea627c117 100644 --- a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt +++ b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt @@ -22,11 +22,12 @@ 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 import app.aaps.plugins.main.general.themes.ThemeSwitcherPlugin -import app.aaps.plugins.main.general.wear.WearPlugin +import app.aaps.plugins.sync.wear.WearPlugin import app.aaps.plugins.main.iob.iobCobCalculator.IobCobCalculatorPlugin import app.aaps.plugins.main.profile.ProfilePlugin import app.aaps.plugins.sensitivity.SensitivityAAPSPlugin @@ -309,12 +310,6 @@ abstract class PluginsListModule { @IntKey(320) abstract fun bindFoodPlugin(plugin: FoodPlugin): PluginBase - @Binds - @AllConfigs - @IntoMap - @IntKey(330) - abstract fun bindWearPlugin(plugin: WearPlugin): PluginBase - @Binds @AllConfigs @IntoMap @@ -340,16 +335,28 @@ abstract class PluginsListModule { abstract fun bindXdripPlugin(plugin: XdripPlugin): PluginBase @Binds - @AllConfigs + @NotNSClient @IntoMap @IntKey(366) + abstract fun bindsOpenHumansPlugin(plugin: OpenHumansUploaderPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(367) + abstract fun bindWearPlugin(plugin: WearPlugin): PluginBase + + @Binds + @AllConfigs + @IntoMap + @IntKey(368) abstract fun bindDataBroadcastPlugin(plugin: DataBroadcastPlugin): PluginBase @Binds - @NotNSClient + @AllConfigs @IntoMap - @IntKey(368) - abstract fun bindsOpenHumansPlugin(plugin: OpenHumansUploaderPlugin): PluginBase + @IntKey(369) + abstract fun bindGarminPlugin(plugin: GarminPlugin): PluginBase @Binds @AllConfigs diff --git a/core/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java b/core/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java index bd7cb5081a..d5af6a5449 100644 --- a/core/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java +++ b/core/graphview/src/main/java/com/jjoe64/graphview/series/BaseSeries.java @@ -140,7 +140,7 @@ public abstract class BaseSeries implements Series * @return the highest y value, or 0 if there is no data */ public double getHighestValueY() { - if (mData.isEmpty()) return 0d; + if (mData.isEmpty()) return 100d; double h = mData.get(0).getY(); for (int i = 1; i < mData.size(); i++) { double c = mData.get(i).getY(); 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/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefFileListProvider.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefFileListProvider.kt index ed0f3745cc..263cf8e052 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefFileListProvider.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/maintenance/PrefFileListProvider.kt @@ -1,6 +1,7 @@ package app.aaps.core.interfaces.maintenance import app.aaps.core.interfaces.rx.weardata.CwfData +import app.aaps.core.interfaces.rx.weardata.CwfFile import java.io.File interface PrefFileListProvider { @@ -13,7 +14,7 @@ interface PrefFileListProvider { fun newExportCsvFile(): File fun newCwfFile(filename: String, withDate: Boolean = true): File fun listPreferenceFiles(loadMetadata: Boolean = false): MutableList - fun listCustomWatchfaceFiles(): MutableList + fun listCustomWatchfaceFiles(): MutableList fun checkMetadata(metadata: Map): Map fun formatExportedAgo(utcTime: String): String } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileDataToWear.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileDataToWear.kt index 6f3ef85b9b..05a7bfcec8 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileDataToWear.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/events/EventMobileDataToWear.kt @@ -2,4 +2,4 @@ package app.aaps.core.interfaces.rx.events import app.aaps.core.interfaces.rx.weardata.EventData -class EventMobileDataToWear(val payload: EventData.ActionSetCustomWatchface) : Event() \ No newline at end of file +class EventMobileDataToWear(val payload: ByteArray) : Event() \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt index d20837ca88..3ef2d4acde 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/CustomWatchfaceFormat.kt @@ -12,6 +12,7 @@ import com.caverock.androidsvg.SVG import kotlinx.serialization.Serializable import org.json.JSONObject import java.io.BufferedOutputStream +import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream @@ -135,9 +136,15 @@ data class ResData(val value: ByteArray, val format: ResFormat) { typealias CwfResDataMap = MutableMap typealias CwfMetadataMap = MutableMap fun CwfResDataMap.isEquals(dataMap: CwfResDataMap) = (this.size == dataMap.size) && this.all { (key, resData) -> dataMap[key]?.value.contentEquals(resData.value) == true } - @Serializable -data class CwfData(val json: String, var metadata: CwfMetadataMap, val resDatas: CwfResDataMap) +data class CwfData(val json: String, var metadata: CwfMetadataMap, val resDatas: CwfResDataMap) { + fun simplify(): CwfData? = resDatas[ResFileMap.CUSTOM_WATCHFACE.fileName]?.let { + val simplifiedDatas: CwfResDataMap = mutableMapOf() + simplifiedDatas[ResFileMap.CUSTOM_WATCHFACE.fileName] = it + CwfData(json, metadata, simplifiedDatas) + } +} +data class CwfFile(val cwfData: CwfData, val zipByteArray: ByteArray) enum class CwfMetadataKey(val key: String, @StringRes val label: Int, val isPref: Boolean) { @@ -252,6 +259,7 @@ enum class JsonKeys(val key: String) { IMAGE("image"), INVALIDIMAGE("invalidImage"), INVALIDCOLOR("invalidColor"), + INVALIDFONTCOLOR("invalidFontColor"), TWINVIEW("twinView"), TOPOFFSETTWINHIDDEN("topOffsetTwinHidden"), LEFTOFFSETTWINHIDDEN("leftOffsetTwinHidden") @@ -286,11 +294,11 @@ class ZipWatchfaceFormat { const val CWF_EXTENTION = ".zip" const val CWF_JSON_FILE = "CustomWatchface.json" - fun loadCustomWatchface(zipInputStream: ZipInputStream, zipName: String, authorization: Boolean): CwfData? { + fun loadCustomWatchface(byteArray: ByteArray, zipName: String, authorization: Boolean): CwfFile? { var json = JSONObject() var metadata: CwfMetadataMap = mutableMapOf() val resDatas: CwfResDataMap = mutableMapOf() - + val zipInputStream = byteArrayToZipInputStream(byteArray) try { var zipEntry: ZipEntry? = zipInputStream.nextEntry while (zipEntry != null) { @@ -324,7 +332,7 @@ class ZipWatchfaceFormat { // Valid CWF file must contains a valid json file with a name within metadata and a custom watchface image return if (metadata.containsKey(CwfMetadataKey.CWF_NAME) && resDatas.containsKey(ResFileMap.CUSTOM_WATCHFACE.fileName)) - CwfData(json.toString(4), metadata, resDatas) + CwfFile(CwfData(json.toString(4), metadata, resDatas), byteArray) else null @@ -368,5 +376,10 @@ class ZipWatchfaceFormat { } return metadata } + + fun byteArrayToZipInputStream(byteArray: ByteArray): ZipInputStream { + val byteArrayInputStream = ByteArrayInputStream(byteArray) + return ZipInputStream(byteArrayInputStream) + } } } \ No newline at end of file diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt index e20a0796b9..744712d139 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/rx/weardata/EventData.kt @@ -292,10 +292,9 @@ sealed class EventData : Event() { } @Serializable - data class ActionSetCustomWatchface( - val customWatchfaceData: CwfData - ) : EventData() - + data class ActionSetCustomWatchface(val customWatchfaceData: CwfData) : EventData() + @Serializable + data class ActionUpdateCustomWatchface(val customWatchfaceData: CwfData) : EventData() @Serializable data class ActionrequestCustomWatchface(val exportFile: Boolean) : EventData() diff --git a/core/interfaces/src/main/res/values-nb-rNO/strings.xml b/core/interfaces/src/main/res/values-nb-rNO/strings.xml index f0d8eeb0e6..4f650e3bf1 100644 --- a/core/interfaces/src/main/res/values-nb-rNO/strings.xml +++ b/core/interfaces/src/main/res/values-nb-rNO/strings.xml @@ -51,7 +51,7 @@ Vis Gj. snitt Delta Vis telefonbatteri Vis riggens batteri - Vis basalrate + Vis basaldose Vis loop status Vis BS Vis BGI @@ -75,7 +75,7 @@ Gj.snitt BS-endring (15min) Telefonbatteri (%) Rig-batteri (%) - Basalrate + Basaldose BGI verdi Tid (TT:MM eller TT:MM:SS) Time (TT) diff --git a/core/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/core/ui/src/main/res/values-nb-rNO/strings.xml b/core/ui/src/main/res/values-nb-rNO/strings.xml index e2d717ae04..af06f65e9f 100644 --- a/core/ui/src/main/res/values-nb-rNO/strings.xml +++ b/core/ui/src/main/res/values-nb-rNO/strings.xml @@ -75,7 +75,7 @@ Insulinets virkningstid (DIA) Insulin-karbohydratfaktor (IK) Insulin sensitivitetsfaktor (ISF) - Basalrate + Basaldose Blodsukkermål g % @@ -193,7 +193,7 @@ %1$d min - Careportal + Helseportal BS-kontroll Manuelt BS eller kalibrering Melding @@ -294,19 +294,19 @@ AVBRYT BOLUS AVBRYT FORLENGET BOLUS AVBRYT MIDL. MÅL - CAREPORTAL + HELSEPORTAL BYTTE SLANGESETT BYTTE RESERVOAR KALIBRERING PRIME BOLUS BEHANDLING - CAREPORTAL NS OPPDATERING + HELSEPORTAL NS-OPPDATERING PROFILBYTTE NS OPPDATERING BEHANDLINGER NS OPPDATERING OPPDATER MIDL. MÅL NS AUTOMASJON FJERNET BS FJERNET - CAREPORTAL FJERNET + HELSEPORTAL FJERNET BOLUS FJERNET KARBO FJERNET MIDL. MÅL FJERNET @@ -486,7 +486,7 @@ Insulinresistent voksen Graviditet Velg pasienttype for oppsett av sikkerhetsgrenser - Maks tillatt bolus [U] + Maks tillatt bolus [E] Maks tillatt karbohydrater [g] Pasienttype @@ -561,9 +561,9 @@ Mangler SMS-tillatelse - Hvordan hindre at appen stenges? + Ikke steng appen min? Opplast av krasjlogger er deaktivert! - \n\nDokumentasjon:\nhttps://androidaps.readthedocs.io\n\nfacebook:\nhttps://www.facebook.com/groups/AndroidAPSUsers + \n\nDokumentasjon:\nhttps://androidaps.readthedocs.io\n\nFacebook:\nhttps://www.facebook.com/groups/AndroidAPSUsers %1$d dag %1$d dager diff --git a/core/utils/src/main/res/values/keys.xml b/core/utils/src/main/res/values/keys.xml index f8e845faa9..b98cdfe2c0 100644 --- a/core/utils/src/main/res/values/keys.xml +++ b/core/utils/src/main/res/values/keys.xml @@ -116,6 +116,9 @@ wearwizard_cob wearwizard_iob wear_custom_watchface_autorization + wear_cwf_watchface_name + wear_cwf_author_version + wear_cwf_filename ObjectivesbgIsAvailableInNS ObjectivespumpStatusIsAvailableInNS statuslights_cage_warning 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/aps/src/main/res/values-nb-rNO/strings.xml b/plugins/aps/src/main/res/values-nb-rNO/strings.xml index c66aa421a2..e508c95944 100644 --- a/plugins/aps/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/aps/src/main/res/values-nb-rNO/strings.xml @@ -41,8 +41,8 @@ Bruk Autosens-funksjon Max E/t en midl. basal kan settes til Denne verdien kalles max basal i OpenAPS - Maksimum basal IOB som OpenAPS kan levere [U] - Denne verdien kalles Max IOB i OpenAPS.\nDet er maks insulinmengde i [U] som APS kan levere i en dose. + Maksimum basal IOB som OpenAPS kan levere [E] + Denne verdien kalles Max IOB i OpenAPS.\nDet er maks insulinmengde i [E] som APS kan levere i en dose. Standard verdi: sann\nGir autosens tillatelse til å justere BS-mål, i tillegg til ISF og basaler. Autosens justerer også BS målverdier Standardverdi er: 3.0 (AMA) eller 8.0 (SMB). Dette er grunninnstillingen for KH-opptak per 5 minutt. Den påvirker hvor raskt COB skal reduseres, og benyttes i beregning av fremtidig BS-kurve når BS enten synker eller øker mer enn forventet. Standardverdi er 3mg/dl/5 min. @@ -54,8 +54,8 @@ Nyttig når data fra ufiltrerte kilder som xDrip+ registrerer mye støy. Multiplikator for maks daglig basal Multiplikator for gjeldende basal - Maks total IOB OpenAPS ikke kan overstige [U] - Denne verdien kalles Maks IOB av OpenAPS\nOpenAPS vil ikke gi mere insulin hvis mengden insulin ombord (IOB) overstiger denne verdien + Maks total IOB OpenAPS ikke kan overstige [E] + Denne verdien kalles Maks IOB av OpenAPS\nAAPS vil ikke gi mere insulin hvis mengden insulin ombord (IOB) overstiger denne verdien Aktiver UAM Aktiver SMB Bruk Supermikrobolus i stedet for midl. basal for raskere resultat @@ -65,7 +65,7 @@ Aktiver SMB etter karbohydrater Aktiver SMB i 6t etter karbohydratinntak, selv med 0 COB. Bare mulig med en bra filtrert BS kilde som f. eks. Dexcom G5/G6 Aktiver SMB med COB - Aktiver SMB når COB er aktiv. + Aktiver SMB når COB (karbohydrater ombord) er aktiv. Aktiver SMB med midl. målverdi Aktiver SMB når midl. målverdi er aktivert (spise snart, trening) Aktiver SMB ved høy midl. målverdi diff --git a/plugins/automation/src/main/res/values-nb-rNO/strings.xml b/plugins/automation/src/main/res/values-nb-rNO/strings.xml index 0b67c24fef..6a838824ae 100644 --- a/plugins/automation/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/automation/src/main/res/values-nb-rNO/strings.xml @@ -87,7 +87,7 @@ COB %1$s %2$.0f Puls HR %1$s %2$.0f - IOB [U]: + IOB [E]: Dist [m]: Gjentakende tidspunkt Hver diff --git a/plugins/configuration/src/main/assets/Analog G-Watch.zip b/plugins/configuration/src/main/assets/Analog G-Watch.zip new file mode 100644 index 0000000000..304a0bd85e Binary files /dev/null and b/plugins/configuration/src/main/assets/Analog G-Watch.zip differ diff --git a/plugins/configuration/src/main/assets/SteamPunk mgdl.zip b/plugins/configuration/src/main/assets/SteamPunk mgdl.zip index a1d1ae1395..f78a49f240 100644 Binary files a/plugins/configuration/src/main/assets/SteamPunk mgdl.zip and b/plugins/configuration/src/main/assets/SteamPunk mgdl.zip differ diff --git a/plugins/configuration/src/main/assets/SteamPunk mmol.zip b/plugins/configuration/src/main/assets/SteamPunk mmol.zip index 1076b2afcf..e15fb0fc2b 100644 Binary files a/plugins/configuration/src/main/assets/SteamPunk mmol.zip and b/plugins/configuration/src/main/assets/SteamPunk mmol.zip differ diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt index fe466ef4e9..33f1162eca 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/PrefFileListProviderImpl.kt @@ -12,6 +12,7 @@ import app.aaps.core.interfaces.maintenance.PrefsMetadataKey import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.weardata.CwfData +import app.aaps.core.interfaces.rx.weardata.CwfFile import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.rx.weardata.ZipWatchfaceFormat import app.aaps.core.interfaces.sharedPreferences.SP @@ -97,11 +98,11 @@ class PrefFileListProviderImpl @Inject constructor( return prefFiles } - override fun listCustomWatchfaceFiles(): MutableList { - val customWatchfaceFiles = mutableListOf() - val customAwtchfaceAuthorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) + override fun listCustomWatchfaceFiles(): MutableList { + val customWatchfaceFiles = mutableListOf() + val customWatchfaceAuthorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) exportsPath.walk().filter { it.isFile && it.name.endsWith(ZipWatchfaceFormat.CWF_EXTENTION) }.forEach { file -> - ZipWatchfaceFormat.loadCustomWatchface(ZipInputStream(file.inputStream()), file.name, customAwtchfaceAuthorization)?.also { customWatchface -> + ZipWatchfaceFormat.loadCustomWatchface(file.readBytes(), file.name, customWatchfaceAuthorization)?.also { customWatchface -> customWatchfaceFiles.add(customWatchface) } } @@ -110,12 +111,11 @@ class PrefFileListProviderImpl @Inject constructor( val assetFiles = context.assets.list("") ?: arrayOf() for (assetFileName in assetFiles) { if (assetFileName.endsWith(ZipWatchfaceFormat.CWF_EXTENTION)) { - val assetInputStream = context.assets.open(assetFileName) - ZipWatchfaceFormat.loadCustomWatchface(ZipInputStream(assetInputStream), assetFileName, customAwtchfaceAuthorization)?.also { customWatchface -> + val assetByteArray = context.assets.open(assetFileName).readBytes() + ZipWatchfaceFormat.loadCustomWatchface(assetByteArray, assetFileName, customWatchfaceAuthorization)?.also { customWatchface -> customWatchfaceFiles.add(customWatchface) - rxBus.send(EventData.ActionGetCustomWatchface(EventData.ActionSetCustomWatchface(customWatchface), exportFile = true, withDate = false)) + rxBus.send(EventData.ActionGetCustomWatchface(EventData.ActionSetCustomWatchface(customWatchface.cwfData), exportFile = true, withDate = false)) } - assetInputStream.close() } } } catch (e: Exception) { diff --git a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt index f42849b993..3858a4135d 100644 --- a/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt +++ b/plugins/configuration/src/main/kotlin/app/aaps/plugins/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt @@ -15,6 +15,7 @@ import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventMobileDataToWear import app.aaps.core.interfaces.rx.weardata.CUSTOM_VERSION import app.aaps.core.interfaces.rx.weardata.CwfData +import app.aaps.core.interfaces.rx.weardata.CwfFile import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_AUTHOR import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_AUTHOR_VERSION import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_CREATED_AT @@ -22,6 +23,7 @@ import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_FILENAME import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_NAME import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey.CWF_VERSION import app.aaps.core.interfaces.rx.weardata.CwfMetadataMap +import app.aaps.core.interfaces.rx.weardata.CwfResDataMap import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.rx.weardata.ResFileMap import app.aaps.core.interfaces.rx.weardata.ZipWatchfaceFormat @@ -56,10 +58,10 @@ class CustomWatchfaceImportListActivity : TranslatedDaggerAppCompatActivity() { supportActionBar?.setDisplayShowTitleEnabled(true) binding.recyclerview.layoutManager = LinearLayoutManager(this) - binding.recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listCustomWatchfaceFiles().sortedBy { it.metadata[CWF_NAME] }) + binding.recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listCustomWatchfaceFiles().sortedBy { it.cwfData.metadata[CWF_NAME] }) } - inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List) : RecyclerView.Adapter() { + inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List) : RecyclerView.Adapter() { inner class CwfFileViewHolder(val customWatchfaceImportListItemBinding: CustomWatchfaceImportListItemBinding) : RecyclerView.ViewHolder(customWatchfaceImportListItemBinding.root) { @@ -67,11 +69,14 @@ class CustomWatchfaceImportListActivity : TranslatedDaggerAppCompatActivity() { with(customWatchfaceImportListItemBinding) { root.isClickable = true customWatchfaceImportListItemBinding.root.setOnClickListener { - val customWatchfaceFile = filelistName.tag as CwfData - val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile) + val customWatchfaceFile = filelistName.tag as CwfFile + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_watchface_name, customWatchfaceFile.cwfData.metadata[CWF_NAME] ?:"") + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_author_version, customWatchfaceFile.cwfData.metadata[CWF_AUTHOR_VERSION] ?:"") + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_filename, customWatchfaceFile.cwfData.metadata[CWF_FILENAME] ?:"") + val i = Intent() setResult(FragmentActivity.RESULT_OK, i) - rxBus.send(EventMobileDataToWear(customWF)) + rxBus.send(EventMobileDataToWear(customWatchfaceFile.zipByteArray)) finish() } } @@ -89,8 +94,8 @@ class CustomWatchfaceImportListActivity : TranslatedDaggerAppCompatActivity() { override fun onBindViewHolder(holder: CwfFileViewHolder, position: Int) { val customWatchfaceFile = customWatchfaceFileList[position] - val metadata = customWatchfaceFile.metadata - val drawable = customWatchfaceFile.resDatas[ResFileMap.CUSTOM_WATCHFACE.fileName]?.toDrawable(resources) + val metadata = customWatchfaceFile.cwfData.metadata + val drawable = customWatchfaceFile.cwfData.resDatas[ResFileMap.CUSTOM_WATCHFACE.fileName]?.toDrawable(resources) with(holder.customWatchfaceImportListItemBinding) { val fileName = metadata[CWF_FILENAME]?.let { "$it${ZipWatchfaceFormat.CWF_EXTENTION}" } ?: "" filelistName.text = rh.gs(app.aaps.core.interfaces.R.string.metadata_wear_import_filename, fileName) diff --git a/plugins/configuration/src/main/res/values-nb-rNO/strings.xml b/plugins/configuration/src/main/res/values-nb-rNO/strings.xml index 3a00df1b2a..f231df03b5 100644 --- a/plugins/configuration/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/configuration/src/main/res/values-nb-rNO/strings.xml @@ -117,12 +117,12 @@ Databaseopprydding Vil du virkelig nullstille databasene? Vedlikeholdsinnstillinger - E-post mottaker + Mottaker av e-post Antall logger du vil sende Send logger via e-post Slett logger - Nightscout versjon: - Engineering Mode aktivert + Nightscout-versjon: + Engineering mode aktivert Loggfiler Logginnstillinger Annet diff --git a/plugins/constraints/src/main/res/values-nb-rNO/exam.xml b/plugins/constraints/src/main/res/values-nb-rNO/exam.xml index 8da0332af8..ee21a0a22c 100644 --- a/plugins/constraints/src/main/res/values-nb-rNO/exam.xml +++ b/plugins/constraints/src/main/res/values-nb-rNO/exam.xml @@ -167,13 +167,13 @@ For å registrere karbohydrater som brukes til å korrigere lavt blodsukker. https://wiki.aaps.app/en/latest/Usage/Extended-Carbs.html Fjernovervåking - Hvordan kan du overvåke AndroidAPS (for eksempel for ditt barn) på eksternt? - AAPSClient app, Nightscout app og Nightscout websiden gjør det mulig for deg å følge AAPS eksternt. + Hvordan kan du eksternt overvåke AndroidAPS (for eksempel for ditt barn)? + Appene AAPSClient og xDrip+ samt Nightscout-websiden gjør det mulig for deg å følge AAPS eksternt. Andre apper (f.eks. Dexcom follow, xDrip som kjører i følger modus) lar deg følge noen parametere (f.eks. blodsukker/sensor verdier) på avstand, men bruker forskjellige beregningsmetoder og kan derfor vise andre IOB eller COB verdier. For å følge AAPS eksternt må begge enhetene ha Internett-tilgang (f.eks. via Wi-Fi eller mobildata). AAPSClient som brukes som ekstern følger app vil både overvåke og gi full kontroll over AAPS. https://wiki.aaps.app/en/latest/Children/Children.html - Insulin Sensitivitetsfaktor (ISF) + Insulinsensitivitetsfaktor (ISF) Økte ISF-verdiene vil føre til levering av mer insulin for å dekke opp en viss mengde karbohydrater. Redusering av ISF verdien vil føre til mer insulintilførsel for å korrigere et blodsukker som ligger over målverdien. Å øke eller senke ISF verdien har ingen effekt på insulintilførselen når blodsukkeret er lavere enn målverdien. @@ -211,7 +211,7 @@ Gjør et profilbytte til mer enn 100%. https://wiki.aaps.app/en/latest/Usage/Profiles.html#timeshift Endring av profil - Basalrater, ISF, KH ratio, etc., bør defineres i profiler. + Basaldoser, ISF, IK-faktor osv. bør defineres i profiler. Aktivering av endringer i Nightscout profilen din krever at din AndroidAPS telefon er koblet til Internett. Å redigere verdier i profilen din er tilstrekkelig for å aktivere profilendringen. Flere profiler kan defineres og velges for å håndtere endringer i omstendigheter (f.eks. hormonelle endringer, skift arbeid, hverdager/helgedager). diff --git a/plugins/constraints/src/main/res/values-nb-rNO/objectives.xml b/plugins/constraints/src/main/res/values-nb-rNO/objectives.xml index 291f461957..1ab33145aa 100644 --- a/plugins/constraints/src/main/res/values-nb-rNO/objectives.xml +++ b/plugins/constraints/src/main/res/values-nb-rNO/objectives.xml @@ -24,7 +24,7 @@ 1 uke vellykket looping på dagtid hvor alle måltider (KH) angis Hvis dine autosens resultater ikke ligger rundt 100% kan det tyde på at profilen din er feil. Aktiver ekstra funksjoner for bruk på dagtid, slik som SMB (Super Micro Bolus) - Du må lese wiki og øke din maxIOB for å få SMB til å fungere. Et godt utgangspunkt er maxIOB = gjennomsnittlig måltidsbolus + 3*max daglig basal + Du må lese wiki og øke din maxIOB for å få SMB til å fungere. Et godt utgangspunkt er å sette maxIOB til gjennomsnittlig måltidsbolus + 3x max daglig basaldose Bruk av SMB er en målsetting. Oref1 algoritmen ble designet for å hjelpe deg med dine bolusdoseringer. Det anbefales å ikke gi full bolusdose for måltider, men bare en del av den, og la AAPS styre resten om nødvendig. På denne måten får du større fleksibilitet med hensyn på feilberegnede KH. Visste du at du kan angi en prosentandel for boluskalkulatoren som resulterer i redusert bolusstørrelse? Aktivere ekstra funksjoner for bruk på dagtid, slik som Dynamisk Sensitivitet Sørg for at SMB fungerer som den skal. Aktiver DynamicISF-tillegget og finn riktig kalibrering for dine behov. Det anbefales å starte med en verdi lavere enn 100% for sikkerhets skyld. diff --git a/plugins/main/build.gradle b/plugins/main/build.gradle index 42103017e3..fa34723f9c 100644 --- a/plugins/main/build.gradle +++ b/plugins/main/build.gradle @@ -45,7 +45,4 @@ dependencies { // Food api "androidx.work:work-runtime-ktx:$work_version" - - // DataLayerListenerService - api "com.google.android.gms:play-services-wearable:$play_services_wearable_version" } \ No newline at end of file diff --git a/plugins/main/src/main/AndroidManifest.xml b/plugins/main/src/main/AndroidManifest.xml index 26e8677ac8..a1e7d61553 100644 --- a/plugins/main/src/main/AndroidManifest.xml +++ b/plugins/main/src/main/AndroidManifest.xml @@ -24,7 +24,7 @@ @@ -35,7 +35,7 @@ android:exported="false" /> 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..b6f2e74936 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 @@ -4,7 +4,6 @@ import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.smsCommunicator.SmsCommunicator import app.aaps.plugins.main.general.persistentNotification.DummyService import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin -import app.aaps.plugins.main.general.wear.WearFragment import app.aaps.plugins.main.iob.iobCobCalculator.IobCobCalculatorPlugin import app.aaps.plugins.main.iob.iobCobCalculator.data.AutosensDataObject import dagger.Binds @@ -21,15 +20,13 @@ import dagger.android.ContributesAndroidInjector SkinsModule::class, SkinsUiModule::class, ActionsModule::class, - WearModule::class, - OverviewModule::class + OverviewModule::class, ] ) @Suppress("unused") abstract class PluginsModule { - @ContributesAndroidInjector abstract fun contributesWearFragment(): WearFragment @ContributesAndroidInjector abstract fun contributesDummyService(): DummyService @ContributesAndroidInjector abstract fun autosensDataObjectInjector(): AutosensDataObject diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/WearModule.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/WearModule.kt deleted file mode 100644 index 12b891b2a3..0000000000 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/di/WearModule.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.aaps.plugins.main.di - -import app.aaps.plugins.main.general.wear.activities.CwfInfosActivity -import app.aaps.plugins.main.general.wear.wearintegration.DataLayerListenerServiceMobile -import dagger.Module -import dagger.android.ContributesAndroidInjector - -@Module -@Suppress("unused") -abstract class WearModule { - - @ContributesAndroidInjector abstract fun contributesWatchUpdaterService(): DataLayerListenerServiceMobile - @ContributesAndroidInjector abstract fun contributesCustomWatchfaceInfosActivity(): CwfInfosActivity -} \ No newline at end of file diff --git a/plugins/main/src/main/res/values-nb-rNO/strings.xml b/plugins/main/src/main/res/values-nb-rNO/strings.xml index 6632e8430a..8fce83a7cb 100644 --- a/plugins/main/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/main/src/main/res/values-nb-rNO/strings.xml @@ -156,7 +156,7 @@ Handlinger Hurtigknapper for rask tilgang til ofte brukte funksjoner - ACT + HAN Midlertidig basal Forlenget bolus Avbryt forlenget bolus @@ -174,18 +174,18 @@ Pumpe Vis statusindikatorer på hjem-skjermen - Terskel for advarsel om alder på slangesett [h] - Terskel for kritisk alder på slangesett [h] - Terskel for advarsel, alder på insulin [h] - Terskel for kritisk alder på insulin [h] - Terskel for advarsel, alder på CGM [h] - Terskel for kritisk alder på CGM [h] + Terskel for advarsel om alder på slangesett [t] + Terskel for kritisk alder på slangesett [t] + Terskel for advarsel, alder på insulin [t] + Terskel for kritisk alder på insulin [t] + Terskel for advarsel, alder på sensor [t] + Terskel for kritisk alder på sensor [t] Terskel for advarsel, batterinivå for sensor [%] Terskel for kritisk batterinivå for sensor [%] - Terskel for advarsel, batterialder for pumpe [h] - Terskel for kritisk batterialder for pumpe [h] - Terskel for advarsel, insulinreservoar [U] - Terskel for kritisk insulinreservoar [U] + Terskel for advarsel, batterialder for pumpe [t] + Terskel for kritisk batterialder for pumpe [t] + Terskel for advarsel, insulinreservoar [E] + Terskel for kritisk insulinreservoar [E] Terskel for advarsel, batterinivå for pumpe [%] Terskel for kritisk batterinivå for pumpe [%] Kopier innstillingene fra NS @@ -242,11 +242,11 @@ Høy verdi Korte navn i menyfaner Vis merknadsfelt i dialogvindu for boluskalkulator - Bolusveiviser utfører beregninger, men bare denne del av beregnet insulin leveres. Nyttig ved bruk av SMB-algoritmen. + Boluskalkulator utfører beregninger, men bare denne del av beregnet insulin leveres. Nyttig ved bruk av SMB-algoritmen. Gi full bolus (100 %) dersom blodsukker er eldre enn Aktiver bolusveileder Bruk en påminnelse om å spise senere istedet for boluskalkulatorens resultat når blodsukker er høyt (\"pre-bolus\") - Aktiver superbolus i veiviser + Aktiver superbolus i boluskalkulator Aktiver superbolus-funksjonen i boluskalkulatoren. Ikke aktiver denne før du vet hvordan den fungerer. DEN KAN LEDE TIL EN OVERDOSERING AV INSULIN HVIS DEN BRUKES UKRITISK! Aktiver boluspåminnelse Bruk en påminnelse for å sette bolusdosen senere med boluskalkulatoren («post bolus») diff --git a/plugins/main/src/main/res/values/strings.xml b/plugins/main/src/main/res/values/strings.xml index 9f1b4c6556..a46b55b81e 100644 --- a/plugins/main/src/main/res/values/strings.xml +++ b/plugins/main/src/main/res/values/strings.xml @@ -330,76 +330,9 @@ Send today\'s log files to developers along with this time. Unexpected situation. IobCobCalculator - - WEAR - Monitor and control AAPS using your WearOS watch. - (No Watch Connected) - Pump status - Loop status - Calc. Wizard:\nInsulin: %1$.2fU\nCarbs: %2$dg - Selected quickwizard no longer available, please refresh your tile - QuickWizard: %1$s\nInsulin: %2$.2fU\nCarbs: %3$dg - Temptarget unknown preset: %1$s - Cancelling running Temp-Targets? - Different units used on watch and phone! - Zero-Temp-Target - cancelling running Temp-Targets? - Min-BG out of range! - Max-BG out of range! - Temptarget:\nMin: %1$s\nMax: %2$s\nDuration: %3$s - Temptarget:\nTarget: %1$s\nDuration: %2$s - Temptarget:\nReason: %1$s\nTarget: %2$s\nDuration: %3$s - not successful - please check phone - Wear settings - Controls from Watch - Set Temp-Targets and enter Treatments from the watch. - Calculations included in the Wizard result: - General Settings - Notify on SMB - Show SMB on the watch like a standard bolus. - Custom Watchface Settings - Custom Watchface Authorization - Authorize loaded custom watchface to change and lock some watch display settings to suit watchface design - Custom Watchface: %1$s - Load Watchface - Infos Watchface - Export template - Custom watchface template exported - Resend All Data - Open Settings on Wear - List of prefs locked by the Watchface - List of prefs required for the Watchface - List of fields included into the Watchface - - Ongoing Notification Shows an ongoing notification with a short overview of what your loop is doing OLD DATA - trying to fetch data from pump. - TDD: Still old data! Cannot load from pump. - g - h - No active profile switch! - Profile:\n\nTimeshift: %1$d\nPercentage: %2$d%%\" - %1$.2fU %1$.0f%% - No profile loaded - Only apply in APS mode! - Last result not available! - CLOSED LOOP - OPEN LOOP - LOOP DISABLED - APS - Last run - Last Enact - Today - weighted - Targets only apply in APS mode! - No history data! - U - Temp Target - until - DEFAULT RANGE - target - Rate: %1$.2fU/h (%2$.2f%%) \nDuration %3$d min diff --git a/plugins/sensitivity/src/main/res/values-nb-rNO/strings.xml b/plugins/sensitivity/src/main/res/values-nb-rNO/strings.xml index 623b93010d..0d1a525bf3 100644 --- a/plugins/sensitivity/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/sensitivity/src/main/res/values-nb-rNO/strings.xml @@ -9,15 +9,15 @@ Sensitivitet beregnes som en vektet gjennomsnittsverdi av avvikene. Ferske avvik har høyere vekting. Minimum opptak av karbohydrater beregnes ut fra maks opptakstid for karbohydrater angitt i dine innstillinger. Denne algoritmen er den raskeste for å justere endringer i sensitivitet. UAM deaktivert fordi den trenger Oref1 sensitivitetsplugin Absorberingsinnstillinger - Maks absorberingstid for måltid [h] + Maks absorberingstid for måltid [t] Tid i timer hvor det forventes at alle karbohydrater fra måltid vil være absorbert - Intervall for autosens [h] + Intervall for autosens [t] Antall timer med historiske data for beregning av sensitivitet (absorpsjonstid for KH er ekskludert) Standardverdi: 1.2\nDette er en multiplikatorbegrensning for autosens (og snart autotune) som begrenser at autosens ikke kan øke med mer enn 20%%, som dermed begrenser hvor mye autosens kan justere opp dine basaler, hvor mye ISF kan reduseres og hvor lavt BS målverdi kan settes. Standardverdi: 0.7\nDette er en multiplikatorbegrensning for autosens-sikkerhet. Den begrenser autosens til å redusere basalverdier, og øke isulinssensitivitet (ISF) og BS mål med ikke mer enn enn 30%. Maks autosens ratio Minimum autosens ratio Standardverdi er: 3.0 (AMA) eller 8.0 (SMB). Dette er grunninnstillingen for KH-opptak per 5 minutt. Den påvirker hvor raskt COB skal reduseres, og benyttes i beregning av fremtidig BS-kurve når BS enten synker eller øker mer enn forventet. Standardverdi er 3mg/dl/5 min. - Maks absorpsjonstid for måltid [h] + Maks absorpsjonstid for måltid [t] Etter denne tiden forventes det at måltidet er absorbert. Eventuelle gjenværende karbo vil tas ut av beregninger. diff --git a/plugins/sync/build.gradle b/plugins/sync/build.gradle index 7fdb1dbd8f..dc8826f9ba 100644 --- a/plugins/sync/build.gradle +++ b/plugins/sync/build.gradle @@ -19,8 +19,9 @@ dependencies { implementation project(':shared:impl') implementation project(':database:entities') implementation project(':database:impl') - implementation project(':core:main') + implementation project(':core:graphview') implementation project(':core:interfaces') + implementation project(':core:main') implementation project(':core:nssdk') implementation project(':core:ui') implementation project(':core:utils') @@ -44,9 +45,11 @@ dependencies { api("io.socket:socket.io-client:2.1.0") api "com.squareup.okhttp3:okhttp:$okhttp3_version" api "com.squareup.okhttp3:logging-interceptor:$okhttp3_version" - //api "com.squareup.retrofit2:retrofit:$retrofit2_version" api "com.squareup.retrofit2:adapter-rxjava3:$retrofit2_version" api "com.squareup.retrofit2:converter-gson:$retrofit2_version" api "com.google.code.gson:gson:$gson_version" + + // DataLayerListenerService + api "com.google.android.gms:play-services-wearable:$play_services_wearable_version" } \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt index a4c8fb3c9e..6ee7128b16 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt @@ -63,7 +63,9 @@ class DataBroadcastPlugin @Inject constructor( ) : PluginBase( PluginDescription() .mainType(PluginType.SYNC) + .pluginIcon(app.aaps.core.main.R.drawable.ic_watch) .pluginName(R.string.data_broadcaster) + .shortName(R.string.data_broadcaster_short) .description(R.string.data_broadcaster_description), aapsLogger, rh, injector ) { 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 a03ea5bece..5fb46c877d 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 @@ -26,6 +28,9 @@ import app.aaps.plugins.sync.nsclientV3.workers.LoadProfileStoreWorker import app.aaps.plugins.sync.nsclientV3.workers.LoadStatusWorker import app.aaps.plugins.sync.nsclientV3.workers.LoadTreatmentsWorker import app.aaps.plugins.sync.tidepool.TidepoolFragment +import app.aaps.plugins.sync.wear.WearFragment +import app.aaps.plugins.sync.wear.activities.CwfInfosActivity +import app.aaps.plugins.sync.wear.wearintegration.DataLayerListenerServiceMobile import app.aaps.plugins.sync.xdrip.DataSyncSelectorXdripImpl import app.aaps.plugins.sync.xdrip.XdripFragment import app.aaps.plugins.sync.xdrip.XdripPlugin @@ -67,6 +72,9 @@ abstract class SyncModule { @ContributesAndroidInjector abstract fun contributesTidepoolFragment(): TidepoolFragment @ContributesAndroidInjector abstract fun contributesXdripFragment(): XdripFragment @ContributesAndroidInjector abstract fun contributesXdripDataSyncWorker(): XdripDataSyncWorker + @ContributesAndroidInjector abstract fun contributesWearFragment(): WearFragment + @ContributesAndroidInjector abstract fun contributesWatchUpdaterService(): DataLayerListenerServiceMobile + @ContributesAndroidInjector abstract fun contributesCustomWatchfaceInfosActivity(): CwfInfosActivity @Module open class Provide { @@ -84,6 +92,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..0b28af0e94 --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -0,0 +1,246 @@ +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) + .pluginIcon(app.aaps.core.main.R.drawable.ic_watch) + .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..c349e3cb3d --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt @@ -0,0 +1,271 @@ +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.BufferedOutputStream +import java.io.ByteArrayOutputStream +import java.io.Closeable +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.lang.Thread.UncaughtExceptionHandler +import java.net.HttpURLConnection +import java.net.Inet4Address +import java.net.InetSocketAddress +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketAddress +import java.net.SocketTimeoutException +import java.net.URI +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: MutableMap CharSequence> = + 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) { + val contentLength = headers["Content-Length"]?.toInt() ?: Int.MAX_VALUE + 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/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearFragment.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearFragment.kt similarity index 96% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearFragment.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearFragment.kt index acda63dfc0..9dc68e02ea 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearFragment.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearFragment.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.wear +package app.aaps.plugins.sync.wear import android.content.Intent import android.os.Bundle @@ -21,9 +21,9 @@ import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.ui.toast.ToastUtils -import app.aaps.plugins.main.R -import app.aaps.plugins.main.databinding.WearFragmentBinding -import app.aaps.plugins.main.general.wear.activities.CwfInfosActivity +import app.aaps.plugins.sync.R +import app.aaps.plugins.sync.databinding.WearFragmentBinding +import app.aaps.plugins.sync.wear.activities.CwfInfosActivity import dagger.android.support.DaggerFragment import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt similarity index 77% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt index 391a731c9c..58461db8c6 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/WearPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/WearPlugin.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.wear +package app.aaps.plugins.sync.wear import android.content.Context import app.aaps.core.interfaces.logging.AAPSLogger @@ -11,7 +11,6 @@ import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventAutosensCalculationFinished import app.aaps.core.interfaces.rx.events.EventDismissBolusProgressIfRunning import app.aaps.core.interfaces.rx.events.EventLoopUpdateGui -import app.aaps.core.interfaces.rx.events.EventMobileDataToWear import app.aaps.core.interfaces.rx.events.EventMobileToWear import app.aaps.core.interfaces.rx.events.EventOverviewBolusProgress import app.aaps.core.interfaces.rx.events.EventPreferenceChange @@ -21,9 +20,9 @@ import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.utils.fabric.FabricPrivacy -import app.aaps.plugins.main.R -import app.aaps.plugins.main.general.wear.wearintegration.DataHandlerMobile -import app.aaps.plugins.main.general.wear.wearintegration.DataLayerListenerServiceMobileHelper +import app.aaps.plugins.sync.R +import app.aaps.plugins.sync.wear.wearintegration.DataHandlerMobile +import app.aaps.plugins.sync.wear.wearintegration.DataLayerListenerServiceMobileHelper import dagger.android.HasAndroidInjector import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign @@ -45,7 +44,7 @@ class WearPlugin @Inject constructor( ) : PluginBase( PluginDescription() - .mainType(PluginType.GENERAL) + .mainType(PluginType.SYNC) .fragmentClass(WearFragment::class.java.name) .pluginIcon(app.aaps.core.main.R.drawable.ic_watch) .pluginName(app.aaps.core.ui.R.string.wear) @@ -112,12 +111,23 @@ class WearPlugin @Inject constructor( fun checkCustomWatchfacePreferences() { savedCustomWatchface?.let { cwf -> - val cwf_authorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) - if (cwf_authorization != cwf.metadata[CwfMetadataKey.CWF_AUTHORIZATION]?.toBooleanStrictOrNull()) { - // resend new customWatchface to Watch with updated authorization for preferences update - val newCwf = cwf.copy() - newCwf.metadata[CwfMetadataKey.CWF_AUTHORIZATION] = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false).toString() - rxBus.send(EventMobileDataToWear(EventData.ActionSetCustomWatchface(newCwf))) + val cwfAuthorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false) + val cwfName = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_watchface_name, "") + val authorVersion = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_author_version, "") + val fileName = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_filename, "") + var toUpdate = false + CwfData("", cwf.metadata, mutableMapOf()).also { + if (cwfAuthorization != cwf.metadata[CwfMetadataKey.CWF_AUTHORIZATION]?.toBooleanStrictOrNull()) { + it.metadata[CwfMetadataKey.CWF_AUTHORIZATION] = cwfAuthorization.toString() + toUpdate = true + } + if (cwfName == cwf.metadata[CwfMetadataKey.CWF_NAME] && authorVersion == cwf.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] && fileName != cwf.metadata[CwfMetadataKey.CWF_FILENAME]) { + it.metadata[CwfMetadataKey.CWF_FILENAME] = fileName + toUpdate = true + } + + if (toUpdate) + rxBus.send(EventMobileToWear(EventData.ActionUpdateCustomWatchface(it))) } } } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/activities/CwfInfosActivity.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/activities/CwfInfosActivity.kt similarity index 96% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/activities/CwfInfosActivity.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/activities/CwfInfosActivity.kt index f12b2975bc..34c0776e31 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/activities/CwfInfosActivity.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/activities/CwfInfosActivity.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.wear.activities +package app.aaps.plugins.sync.wear.activities import android.os.Bundle import android.view.LayoutInflater @@ -24,11 +24,11 @@ import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.core.interfaces.versionChecker.VersionCheckerUtils import app.aaps.core.ui.activities.TranslatedDaggerAppCompatActivity -import app.aaps.plugins.main.R -import app.aaps.plugins.main.databinding.CwfInfosActivityBinding -import app.aaps.plugins.main.databinding.CwfInfosActivityPrefItemBinding -import app.aaps.plugins.main.databinding.CwfInfosActivityViewItemBinding -import app.aaps.plugins.main.general.wear.WearPlugin +import app.aaps.plugins.sync.R +import app.aaps.plugins.sync.databinding.CwfInfosActivityBinding +import app.aaps.plugins.sync.databinding.CwfInfosActivityPrefItemBinding +import app.aaps.plugins.sync.databinding.CwfInfosActivityViewItemBinding +import app.aaps.plugins.sync.wear.WearPlugin import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import org.json.JSONObject @@ -192,7 +192,7 @@ class CwfInfosActivity : TranslatedDaggerAppCompatActivity() { val visibleKeyPairs = mutableListOf>() - for (viewKey in ViewKeys.values()) { + for (viewKey in ViewKeys.entries) { try { val jsonValue = json.optJSONObject(viewKey.key) if (jsonValue != null) { diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt similarity index 98% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt index c55939ecbc..8724ca92af 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataHandlerMobile.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.wear.wearintegration +package app.aaps.plugins.sync.wear.wearintegration import android.app.NotificationManager import android.content.Context @@ -31,6 +31,7 @@ import app.aaps.core.interfaces.rx.AapsSchedulers import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventMobileToWear import app.aaps.core.interfaces.rx.events.EventWearUpdateGui +import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.ui.UiInteraction @@ -66,7 +67,7 @@ import app.aaps.database.impl.AppRepository import app.aaps.database.impl.transactions.CancelCurrentTemporaryTargetIfAnyTransaction import app.aaps.database.impl.transactions.InsertAndCancelCurrentTemporaryTargetTransaction import app.aaps.database.impl.transactions.InsertOrUpdateHeartRateTransaction -import app.aaps.plugins.main.R +import app.aaps.plugins.sync.R import dagger.android.HasAndroidInjector import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign @@ -1266,10 +1267,19 @@ class DataHandlerMobile @Inject constructor( private fun handleGetCustomWatchface(command: EventData.ActionGetCustomWatchface) { val customWatchface = command.customWatchface - aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.customWatchfaceData.json}") - rxBus.send(EventWearUpdateGui(customWatchface.customWatchfaceData, command.exportFile)) + aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}") + val cwfData = customWatchface.customWatchfaceData + rxBus.send(EventWearUpdateGui(cwfData, command.exportFile)) + val watchfaceName = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_watchface_name, "") + val authorVersion = sp.getString(app.aaps.core.utils.R.string.key_wear_cwf_author_version, "") + if (cwfData.metadata[CwfMetadataKey.CWF_NAME] != watchfaceName || cwfData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] != authorVersion) { + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_watchface_name, cwfData.metadata[CwfMetadataKey.CWF_NAME] ?:"") + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_author_version, cwfData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] ?:"") + sp.putString(app.aaps.core.utils.R.string.key_wear_cwf_filename, cwfData.metadata[CwfMetadataKey.CWF_FILENAME] ?:"") + } + if (command.exportFile) - importExportPrefs.exportCustomWatchface(customWatchface.customWatchfaceData, command.withDate) + importExportPrefs.exportCustomWatchface(cwfData, command.withDate) } } diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt similarity index 97% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt index ec3685409c..92fdea324e 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobile.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobile.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.wear.wearintegration +package app.aaps.plugins.sync.wear.wearintegration import android.os.Binder import android.os.Handler @@ -20,8 +20,8 @@ import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.utils.fabric.FabricPrivacy import app.aaps.database.impl.AppRepository -import app.aaps.plugins.main.R -import app.aaps.plugins.main.general.wear.WearPlugin +import app.aaps.plugins.sync.R +import app.aaps.plugins.sync.wear.WearPlugin import com.google.android.gms.tasks.Tasks import com.google.android.gms.wearable.CapabilityClient import com.google.android.gms.wearable.CapabilityInfo @@ -93,7 +93,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() { disposable += rxBus .toObservable(EventMobileDataToWear::class.java) .observeOn(aapsSchedulers.io) - .subscribe { sendMessage(rxDataPath, it.payload.serializeByte()) } + .subscribe { sendMessage(rxDataPath, it.payload) } } override fun onCapabilityChanged(p0: CapabilityInfo) { diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobileHelper.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobileHelper.kt similarity index 97% rename from plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobileHelper.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobileHelper.kt index 68cf866205..302e022962 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/wear/wearintegration/DataLayerListenerServiceMobileHelper.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/wear/wearintegration/DataLayerListenerServiceMobileHelper.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.main.general.wear.wearintegration +package app.aaps.plugins.sync.wear.wearintegration import android.content.ComponentName import android.content.Context diff --git a/plugins/main/src/main/res/drawable/settings_off.xml b/plugins/sync/src/main/res/drawable/settings_off.xml similarity index 100% rename from plugins/main/src/main/res/drawable/settings_off.xml rename to plugins/sync/src/main/res/drawable/settings_off.xml diff --git a/plugins/main/src/main/res/drawable/settings_on.xml b/plugins/sync/src/main/res/drawable/settings_on.xml similarity index 100% rename from plugins/main/src/main/res/drawable/settings_on.xml rename to plugins/sync/src/main/res/drawable/settings_on.xml diff --git a/plugins/main/src/main/res/layout/cwf_infos_activity.xml b/plugins/sync/src/main/res/layout/cwf_infos_activity.xml similarity index 94% rename from plugins/main/src/main/res/layout/cwf_infos_activity.xml rename to plugins/sync/src/main/res/layout/cwf_infos_activity.xml index 8648bea857..68bc98a6c1 100644 --- a/plugins/main/src/main/res/layout/cwf_infos_activity.xml +++ b/plugins/sync/src/main/res/layout/cwf_infos_activity.xml @@ -6,7 +6,7 @@ android:scrollbarStyle="outsideOverlay" android:scrollbars="vertical" android:paddingTop="2dp" - tools:context=".general.wear.activities.CwfInfosActivity"> + tools:context="app.aaps.plugins.sync.wear.activities.CwfInfosActivity"> + android:textSize="18sp" + tools:ignore="HardcodedText" /> + android:textSize="18sp" + tools:ignore="HardcodedText" /> + android:textSize="18sp" + tools:ignore="HardcodedText" /> + android:textSize="18sp" + tools:ignore="HardcodedText" /> + tools:context="app.aaps.plugins.sync.wear.WearFragment"> Boluser Forlenget bolus Karbohydrater - Careportal hendelser (unntatt notater) + Helseportal-hendelser (unntatt notater) Profilbytter Totale daglige doser Midlertidige basal doser diff --git a/plugins/sync/src/main/res/values-nb-rNO/strings.xml b/plugins/sync/src/main/res/values-nb-rNO/strings.xml index e91f3141de..dbed92b638 100644 --- a/plugins/sync/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/sync/src/main/res/values-nb-rNO/strings.xml @@ -11,7 +11,7 @@ Tillat tilkobling i roaming Lag meldinger ved feil Opprett varslinger hvis det er nødvendig med karbohydrater - Opprett varslinger i Nightscout ved feil eller meldinger (også synlig i Careportal under Behandlinger) + Opprett varslinger i Nightscout ved feil eller meldinger (også synlig i Helseportal under Behandlinger) Opprett Nightscout-meldinger ved behov for karbohydrater Synkroniserer dine data med Nightscout v1 API Synkroniserer dine data med Nightscout v3 API @@ -54,11 +54,11 @@ Motta midlertidige mål Aksepter midlertidige mål angitt med NS eller AAPSClient Motta profilbytter - Aksepter profilbytter som er angitt via NS eller NSClient + Aksepter profilbytter som er angitt via NS eller AAPSClient Motta APS offline hendelser Aksepter APS offline hendelser lagt inn gjennom NS eller AAPSClient Motta TBR og EB - Godta TBR og EB beregninger fra tilleggsmodul + Godta midlertidig basal og forlenget bolus lagt inn fra en annen instans Motta insulin Aksepter insulin angitt via NS eller AAPSClient (enhetene er ikke dosert, kun beregnet mot IOB) Motta karbohydrater diff --git a/plugins/sync/src/main/res/values/strings.xml b/plugins/sync/src/main/res/values/strings.xml index 72f0a1e164..015419cce4 100644 --- a/plugins/sync/src/main/res/values/strings.xml +++ b/plugins/sync/src/main/res/values/strings.xml @@ -180,6 +180,78 @@ Data Broadcaster - Broadcast data to other apps like Garmin watch + DBRO + Broadcast data to Garmin\'s G-Watch Wear App + + + Garmin + Connection to Garmin device (Fenix, Edge, …) + Garmin settings + + + WEAR + Monitor and control AAPS using your WearOS watch. + (No Watch Connected) + Pump status + Loop status + Calc. Wizard:\nInsulin: %1$.2fU\nCarbs: %2$dg + Selected quickwizard no longer available, please refresh your tile + QuickWizard: %1$s\nInsulin: %2$.2fU\nCarbs: %3$dg + Temptarget unknown preset: %1$s + Cancelling running Temp-Targets? + Different units used on watch and phone! + Zero-Temp-Target - cancelling running Temp-Targets? + Min-BG out of range! + Max-BG out of range! + Temptarget:\nMin: %1$s\nMax: %2$s\nDuration: %3$s + Temptarget:\nTarget: %1$s\nDuration: %2$s + Temptarget:\nReason: %1$s\nTarget: %2$s\nDuration: %3$s + not successful - please check phone + Wear settings + Controls from Watch + Set Temp-Targets and enter Treatments from the watch. + Calculations included in the Wizard result: + General Settings + Notify on SMB + Show SMB on the watch like a standard bolus. + Custom Watchface Settings + Custom Watchface Authorization + Authorize loaded custom watchface to change and lock some watch display settings to suit watchface design + Custom Watchface: %1$s + Load Watchface + Infos Watchface + Export template + Custom watchface template exported + Resend All Data + Open Settings on Wear + List of prefs locked by the Watchface + List of prefs required for the Watchface + List of fields included into the Watchface + trying to fetch data from pump. + TDD: Still old data! Cannot load from pump. + U + g + h + No active profile switch! + Profile:\n\nTimeshift: %1$d\nPercentage: %2$d%%\" + Targets only apply in APS mode! + No history data! + Temp Target + until + DEFAULT RANGE + target + Rate: %1$.2fU/h (%2$.2f%%) \nDuration %3$d min + No profile loaded + Only apply in APS mode! + Last result not available! + CLOSED LOOP + OPEN LOOP + LOOP DISABLED + APS + Last run + Last Enact + %1$.2fU %1$.0f%% + Today + weighted \ 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/main/src/main/res/xml/pref_wear.xml b/plugins/sync/src/main/res/xml/pref_wear.xml similarity index 100% rename from plugins/main/src/main/res/xml/pref_wear.xml rename to plugins/sync/src/main/res/xml/pref_wear.xml 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 diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/DeviceStatusExtensionKtTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/DeviceStatusExtensionKtTest.kt index f1cdf04ba3..1213d40efc 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/DeviceStatusExtensionKtTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/nsclientV3/extensions/DeviceStatusExtensionKtTest.kt @@ -5,6 +5,7 @@ import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData import app.aaps.core.interfaces.objects.Instantiator import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.sharedPreferences.SP +import app.aaps.core.interfaces.ui.UiInteraction import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.nssdk.interfaces.RunningConfiguration import app.aaps.core.nssdk.mapper.convertToRemoteAndBack @@ -16,6 +17,7 @@ import com.google.common.truth.Truth.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.Mock +import org.mockito.Mockito @Suppress("SpellCheckingInspection") internal class DeviceStatusExtensionKtTest : TestBase() { @@ -26,6 +28,7 @@ internal class DeviceStatusExtensionKtTest : TestBase() { @Mock lateinit var config: Config @Mock lateinit var runningConfiguration: RunningConfiguration @Mock lateinit var instantiator: Instantiator + @Mock lateinit var uiInteraction: UiInteraction private lateinit var processedDeviceStatusData: ProcessedDeviceStatusData private lateinit var nsDeviceStatusHandler: NSDeviceStatusHandler @@ -33,7 +36,8 @@ internal class DeviceStatusExtensionKtTest : TestBase() { @BeforeEach fun setup() { processedDeviceStatusData = ProcessedDeviceStatusDataImpl(rh, dateUtil, sp, instantiator) - nsDeviceStatusHandler = NSDeviceStatusHandler(sp, config, dateUtil, runningConfiguration, processedDeviceStatusData) + nsDeviceStatusHandler = NSDeviceStatusHandler(sp, config, dateUtil, runningConfiguration, processedDeviceStatusData, uiInteraction, rh) + Mockito.`when`(config.NSCLIENT).thenReturn(true) } @Test diff --git a/pump/combo/src/main/res/values-nb-rNO/strings.xml b/pump/combo/src/main/res/values-nb-rNO/strings.xml index 1883de382d..b13cd45cd5 100644 --- a/pump/combo/src/main/res/values-nb-rNO/strings.xml +++ b/pump/combo/src/main/res/values-nb-rNO/strings.xml @@ -36,13 +36,13 @@ Ugyldig oppsett av pumpen. Les dokumentasjonen og sjekk at Quick Info menyen heter QUICK INFO ved hjelp av 360 programvaren. Leser basalprofil Pumpe historikken har blitt endret siden bolus kalkuleringen ble utført. Bolus har ikke blitt levert. Vennligst rekalkuler om bolus fortsatt er nødvendig. - Bolus har blitt levert, men det oppsto en feil ved loggføring i behandlinger. Dette kan oppstå hvis to små bolus på samme størrelse blir levert i løpet av to minutter. Vennligst sjekk pumpe historikken og behandlinger loggen, og bruk Careportal for å legge til de manglende behandlingene. Pass på at du ikke legger til to identiske behandlinger på samme minutt. + Bolus har blitt levert, men det oppsto en feil ved loggføring i behandlinger. Dette kan oppstå hvis to små bolus på samme størrelse blir levert i løpet av to minutter. Vennligst sjekk pumpe historikken og behandlinger loggen, og bruk Helseportal for å legge til de manglende behandlingene. Pass på at du ikke legger til to identiske behandlinger på samme minutt. Avviser høy temp target siden kalkuleringen ikke tok hensyn til nylige endringer i pumpe historikken Oppdaterer pumpestatus Basal dosen i pumpen har blitt endret og vil i løpet av kort tid bli oppdatert Basalsats endret i pumpe, men lesing av den feilet Sjekker for endringer i historikken - Flere boluser levert i samme minutt og med samme insulinmengde ble importert. Bare en av doseringene ble lagt til i behandlinger. Vennligst sjekk pumpen og legg manuelt til ekstra bolus doseringer i Careportal. Ikke legg til flere boluser i samme minutt. + Flere boluser levert i samme minutt og med samme insulinmengde ble importert. Bare en av doseringene ble lagt til i behandlinger. Vennligst sjekk pumpen og legg manuelt til ekstra bolus doseringer i Helseportal. Ikke legg til flere boluser i samme minutt. Den siste bolus er eldre enn 24t eller er i fremtiden. Vennligst sjekk at datoen i pumpen er korrekt. Tid/dato for levert bolus i pumpen er trolig feil, og IOB beregningen blir da feil. Vennligst sjekk pumpens tid/dato. Antall boluser diff --git a/pump/combov2/src/main/res/values-nb-rNO/strings.xml b/pump/combov2/src/main/res/values-nb-rNO/strings.xml index c31eb9b14f..e905e4cfb5 100644 --- a/pump/combov2/src/main/res/values-nb-rNO/strings.xml +++ b/pump/combov2/src/main/res/values-nb-rNO/strings.xml @@ -100,7 +100,7 @@ knappene samtidig for å avbryte parringen)\n Lar aktive emulert 100% TBR få avslutte Ignorerer redundant 100% TBR forespørsel Uventet begrensning oppsto ved justering av TBR: målprosenten var %1$d%%, nådde grense på %2$d%% - Kan ikke sette absolutt TBR hvis basalraten er null + Kan ikke sette absolutt TBR hvis basaldosen er null Sammenkoble AndroidAPS og Android med en ikke-tilkoblet Accu-Chek Combo pumpe Koble fra AndroidAPS og Android fra den ilkoblede Accu-Chek Combo pumpen Ukjent TBR ble oppdaget og stoppet; prosent: %1$d%%, gjenværende varighet: %2$s diff --git a/pump/dana/src/main/res/values-nb-rNO/strings.xml b/pump/dana/src/main/res/values-nb-rNO/strings.xml index 211c2198e2..e561897b03 100644 --- a/pump/dana/src/main/res/values-nb-rNO/strings.xml +++ b/pump/dana/src/main/res/values-nb-rNO/strings.xml @@ -107,9 +107,9 @@ Bolus hastighet Valgt pumpe Logg reservoar bytte - Legg til \"Insulinbytte\" i Careportal når den oppdages i historikken + Legg til \"Insulinbytte\" i Helseportal når den oppdages i historikken Logg kanyle bytte - Legg til \"Kanylebytte\" i Careportal når den oppdages i historikken + Legg til \"Kanylebytte\" i Helseportal når den oppdages i historikken PIN1 PIN2 Trykk OK på pumpen\nog skriv inn de 2 viste tallene\nHold skjermen på pumpen PÅ ved å trykke minus knappen til du fullfører inntastingen. diff --git a/pump/diaconn/src/main/res/values-nb-rNO/strings.xml b/pump/diaconn/src/main/res/values-nb-rNO/strings.xml index 23b32d57b3..744ca26447 100644 --- a/pump/diaconn/src/main/res/values-nb-rNO/strings.xml +++ b/pump/diaconn/src/main/res/values-nb-rNO/strings.xml @@ -85,10 +85,10 @@ aps_incarnation_no pump_serial_no Logg reservoar bytte - Legg til \"Insulinbytte\" i Careportal når den oppdages i historikken + Legg til \"Insulinbytte\" i Helseportal når den oppdages i historikken Logg bytte av kanyle - Legg til \"Bytte av injeksjonssted\" i Careportal når den oppdages i historikken - Legg til \"Bytte av batteri\" i Careportal når den oppdages i historikken + Legg til \"Bytte av injeksjonssted\" i Helseportal når den oppdages i historikken + Legg til \"Bytte av batteri\" i Helseportal når den oppdages i historikken Logg batteri bytte Logg synkronisering pågår Lavt insulinnivå @@ -145,7 +145,7 @@ Når basal oppsett er fullført, kan basaldoseringer startes. Kommandoen ble ikke utført. Vennligst prøv igjen. Logg bytte av slangesett - Legg til \"Slangesettbytte\" i Careportal når den oppdages i historikken + Legg til \"Slangesettbytte\" i Helseportal når den oppdages i historikken Temp Basal startet Vel Lav Glukose Stopp (LGS) er injeksjoner begrenset LGS status er PÅ. PÅ kommando er nektet. diff --git a/pump/medtrum/src/main/res/values-nb-rNO/strings.xml b/pump/medtrum/src/main/res/values-nb-rNO/strings.xml index c72e66c4d5..8100bc66f2 100644 --- a/pump/medtrum/src/main/res/values-nb-rNO/strings.xml +++ b/pump/medtrum/src/main/res/values-nb-rNO/strings.xml @@ -21,7 +21,7 @@ %.2f E %.2f V Basal type - Basalrate + Basaldose %.2f E/t Pumpetype FW versjon @@ -85,7 +85,7 @@ Trykk Neste for å fortsette. Trykk Neste for å starte aktivering. Fjern sikkerhetslåsen. Koble pumpen til kroppen. Trykk nål-knappen. - Aktiverer pumpe og setter basalrate. Vennligst vent. + Aktiverer pumpe og setter basaldose. Vennligst vent. Kunne ikke aktivere, trykk Prøv igjen for å prøve igjen. Ny patch aktivert. %.2f enheter gjenstår. Trykk OK for å gå tilbake til hovedskjermen. diff --git a/pump/medtrum/src/main/res/values-nl-rNL/strings.xml b/pump/medtrum/src/main/res/values-nl-rNL/strings.xml index 1695ea051f..c566589058 100644 --- a/pump/medtrum/src/main/res/values-nl-rNL/strings.xml +++ b/pump/medtrum/src/main/res/values-nl-rNL/strings.xml @@ -53,6 +53,7 @@ Batterij leeg Geen kalibratie Update van pomp tijdzone mislukt, snooze bericht en vernieuw handmatig. + Bolus fout Opnieuw Volgende diff --git a/pump/medtrum/src/main/res/values-ro-rRO/strings.xml b/pump/medtrum/src/main/res/values-ro-rRO/strings.xml index 1b55a6cc25..01b5297304 100644 --- a/pump/medtrum/src/main/res/values-ro-rRO/strings.xml +++ b/pump/medtrum/src/main/res/values-ro-rRO/strings.xml @@ -53,6 +53,7 @@ Baterie descărcată Fără calibrare Actualizarea fusului orar al pompei a eșuat, amână mesajul și actualizează manual. + Eroare bolus Încearcă din nou Înainte @@ -101,6 +102,8 @@ Apasă Înainte pentru a relua activarea sau Renunțare pentru a reseta statusul activării. Te rog, așteaptă. Se citește starea de activare din pompă. + Alertă de inaccesibilitate activată forțat, deoarece patchul Medtrum poate eșua și să nu fie accesibil. + Recomandat să se seteze la 30 de minute, deoarece patchul Medtrum poate eșua și să nu fie accesibil. Număr de serie Introdu numărul de serie al bazei pompei. Număr de serie invalid! diff --git a/pump/medtrum/src/main/res/values-ru-rRU/strings.xml b/pump/medtrum/src/main/res/values-ru-rRU/strings.xml index 533755408c..c029351280 100644 --- a/pump/medtrum/src/main/res/values-ru-rRU/strings.xml +++ b/pump/medtrum/src/main/res/values-ru-rRU/strings.xml @@ -6,6 +6,7 @@ Интеграция с помпами Medtrum Nano и Medtrum 300U Настройки Medtrum Ошибка помпы: %1$s !! + Помпа - Предупреждение: %1$s Помпа приостановлена Помпа приостановлена из-за превышения максимального количества инсулина в час Помпа приостановлена в связи с превышением максимального допустимого количества инсулина в сутки @@ -52,6 +53,7 @@ Батарея разряжена Нет калибровки Не удалось обновить часовой пояс помпы, закройте сообщение и обновите вручную. + Болюс - ошибка Повторить Далее @@ -100,12 +102,16 @@ Нажмите Далее, чтобы возобновить активацию или Discard для сброса статуса активации. Пожалуйста, подождите, получение статуса активации с помпы. + Принудительно включена сигнализация о недоступности помпы, так как патч Medtrum может выйти из строя и быть недоступным. + Рекомендуется установить значение в 30 минут, так как патч Medtrum может выйти из строя и быть недоступным. Серийный номер Введите серийный номер основания вашей помпы. Неверный серийный номер! Помпа не тестировалась: %1$d! Свяжитесь с нами в discord или github Настройки оповещений Выберите предпочитаемые параметры оповещений помпы. + Уведомление об оповещениях помпы + Показывать уведомления о некритических оповещениях помпы: низком заряде батареи, низком запасе инсулина (20 единиц) и приближающемся истечении срока работы. Рекомендуется оставить включенным, когда звук оповещений выключен. Окончание срока действия патча Если включено, то патч закончит свое действие через 3 дня, с дополнительными 8 часами после этого. Максимальное количество инсулина в час diff --git a/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/common/activity/OmnipodWizardActivityBase.kt b/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/common/activity/OmnipodWizardActivityBase.kt index 4bcd349a12..2628c2d0d3 100644 --- a/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/common/activity/OmnipodWizardActivityBase.kt +++ b/pump/omnipod-common/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/common/ui/wizard/common/activity/OmnipodWizardActivityBase.kt @@ -1,6 +1,7 @@ package info.nightscout.androidaps.plugins.pump.omnipod.common.ui.wizard.common.activity import android.os.Bundle +import android.view.WindowManager import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AlertDialog import androidx.navigation.NavController @@ -12,6 +13,7 @@ abstract class OmnipodWizardActivityBase : TranslatedDaggerAppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { exitActivityAfterConfirmation() diff --git a/pump/omnipod-dash/src/main/res/values-nb-rNO/strings.xml b/pump/omnipod-dash/src/main/res/values-nb-rNO/strings.xml index dffebca5c9..b7b4c84bed 100644 --- a/pump/omnipod-dash/src/main/res/values-nb-rNO/strings.xml +++ b/pump/omnipod-dash/src/main/res/values-nb-rNO/strings.xml @@ -43,7 +43,7 @@ Profilen ble angitt Pausing av insulintilførsel ble ikke bekreftet! Vennligst oppdater Pod-status fra Omnipod-fanen og gjenoppta insulinlevering om nødvendig. Insulintilførsel er pauset - Tidssone på pod er forskjellig fra tidssonen på telefon. Basalrater er feil. Bytt profil for å korrigere + Tidssone på pod er forskjellig fra tidssonen på telefon. Basaldoser er feil. Bytt profil for å korrigere Kunne ikke sette ny basalprofil. Insulintilførsel er pauset Endring av basalprofil kan ha feilet. Insulinlevering kan bli stoppet! Vennligst velg Oppdater Pod fra Omnipod-fanen og velg gjenoppta levering hvis nødvendig. Status for levering av bolusdoser er usikker. Oppdater pod-statusen for å verifisere. diff --git a/ui/src/main/res/layout/dialog_wizard.xml b/ui/src/main/res/layout/dialog_wizard.xml index 6da6b7c2b3..885d3204f9 100644 --- a/ui/src/main/res/layout/dialog_wizard.xml +++ b/ui/src/main/res/layout/dialog_wizard.xml @@ -38,6 +38,43 @@ + + + + + + + + - - - - - - - - diff --git a/ui/src/main/res/values-nb-rNO/strings.xml b/ui/src/main/res/values-nb-rNO/strings.xml index 09efac2ec8..206e1310d4 100644 --- a/ui/src/main/res/values-nb-rNO/strings.xml +++ b/ui/src/main/res/values-nb-rNO/strings.xml @@ -124,7 +124,7 @@ DPV standardprofil Ugyldig % verdi - Basalrate + Basaldose STOPP er trykket diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt index 119fc92587..038f2124f8 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataHandlerWear.kt @@ -186,7 +186,17 @@ class DataHandlerWear @Inject constructor( .subscribe { aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${it.sourceNodeId}") persistence.store(it) - persistence.readCustomWatchface()?.let { + persistence.readSimplifiedCwf()?.let { + rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, false))) + } + } + disposable += rxBus + .toObservable(EventData.ActionUpdateCustomWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { + aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${it.sourceNodeId}") + persistence.store(it) + persistence.readSimplifiedCwf()?.let { rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, false))) } } @@ -205,7 +215,7 @@ class DataHandlerWear @Inject constructor( .observeOn(aapsSchedulers.io) .subscribe { eventData -> aapsLogger.debug(LTag.WEAR, "Custom Watchface requested from ${eventData.sourceNodeId} export ${eventData.exportFile}") - persistence.readCustomWatchface(eventData.exportFile)?.let { + persistence.readSimplifiedCwf(eventData.exportFile)?.let { rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, eventData.exportFile))) } } diff --git a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt index 11914555ad..c701b69391 100644 --- a/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt +++ b/wear/src/main/kotlin/app/aaps/wear/comm/DataLayerListenerServiceWear.kt @@ -12,6 +12,7 @@ import app.aaps.core.interfaces.rx.bus.RxBus import app.aaps.core.interfaces.rx.events.EventWearDataToMobile import app.aaps.core.interfaces.rx.events.EventWearToMobile import app.aaps.core.interfaces.rx.weardata.EventData +import app.aaps.core.interfaces.rx.weardata.ZipWatchfaceFormat import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.wear.interaction.utils.Persistence import app.aaps.wear.interaction.utils.WearUtil @@ -127,9 +128,11 @@ class DataLayerListenerServiceWear : WearableListenerService() { } rxDataPath -> { - aapsLogger.debug(LTag.WEAR, "onMessageReceived: ${messageEvent.data}") - val command = EventData.deserializeByte(messageEvent.data) - rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) + aapsLogger.debug(LTag.WEAR, "onMessageReceived: ${messageEvent.data.size}") + ZipWatchfaceFormat.loadCustomWatchface(messageEvent.data, "", false)?.let { + val command = EventData.ActionSetCustomWatchface(it.cwfData) + rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) + } // Use this sender transcriptionNodeId = messageEvent.sourceNodeId aapsLogger.debug(LTag.WEAR, "Updated node: $transcriptionNodeId") diff --git a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt index 67dcf70797..dcf074a80a 100644 --- a/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt +++ b/wear/src/main/kotlin/app/aaps/wear/interaction/utils/Persistence.kt @@ -3,6 +3,9 @@ package app.aaps.wear.interaction.utils import app.aaps.annotations.OpenForTesting import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.rx.events.EventMobileToWear +import app.aaps.core.interfaces.rx.weardata.CwfData +import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey import app.aaps.core.interfaces.rx.weardata.EventData import app.aaps.core.interfaces.rx.weardata.EventData.Companion.deserialize import app.aaps.core.interfaces.rx.weardata.EventData.SingleBg @@ -149,6 +152,26 @@ open class Persistence @Inject constructor( return null } + fun readSimplifiedCwf(isDefault: Boolean = false): EventData.ActionSetCustomWatchface? { + try { + var s = sp.getStringOrNull(if (isDefault) CUSTOM_DEFAULT_WATCHFACE else CUSTOM_WATCHFACE, null) + if (s != null) { + return (deserialize(s) as EventData.ActionSetCustomWatchface).let { + EventData.ActionSetCustomWatchface(it.customWatchfaceData.simplify() ?:it.customWatchfaceData) + } + + } else { + s = sp.getStringOrNull(CUSTOM_DEFAULT_WATCHFACE, null) + if (s != null) { + return deserialize(s) as EventData.ActionSetCustomWatchface + } + } + } catch (exception: Exception) { + aapsLogger.error(LTag.WEAR, exception.toString()) + } + return null + } + fun store(singleBg: SingleBg) { putString(BG_DATA_PERSISTENCE_KEY, singleBg.serialize()) aapsLogger.debug(LTag.WEAR, "Stored BG data: $singleBg") @@ -175,6 +198,21 @@ open class Persistence @Inject constructor( aapsLogger.debug(LTag.WEAR, "Stored Custom Watchface ${customWatchface.customWatchfaceData} ${isdefault}: $customWatchface") } + fun store(customWatchface: EventData.ActionUpdateCustomWatchface) { + readCustomWatchface()?.let { savedCwData -> + if (customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_NAME] && + customWatchface.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] == savedCwData.customWatchfaceData.metadata[CwfMetadataKey.CWF_AUTHOR_VERSION] + ) { + // if same name and author version, then resync metadata to watch to update filename and authorization + val newCwfData = CwfData(savedCwData.customWatchfaceData.json, customWatchface.customWatchfaceData.metadata, savedCwData.customWatchfaceData.resDatas) + EventData.ActionSetCustomWatchface(newCwfData).also { + putString(CUSTOM_WATCHFACE, it.serialize()) + aapsLogger.debug(LTag.WEAR, "Update Custom Watchface ${it.customWatchfaceData} : $customWatchface") + } + } + } +} + fun setDefaultWatchface() { readCustomWatchface(true)?.let { store(it) } aapsLogger.debug(LTag.WEAR, "Custom Watchface reset to default") diff --git a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt index 345812cb27..6f7e93d53a 100644 --- a/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt +++ b/wear/src/main/kotlin/app/aaps/wear/watchfaces/CustomWatchface.kt @@ -106,13 +106,13 @@ class CustomWatchface : BaseWatchFace() { override fun setColorDark() { setWatchfaceStyle() - if ((ViewMap.SGV.dynData?.stepColor ?: 0) == 0) + if ((ViewMap.SGV.dynData?.stepFontColor ?: 0) == 0) binding.sgv.setTextColor(bgColor) if ((ViewMap.DIRECTION.dynData?.stepColor ?: 0) == 0) binding.direction2.colorFilter = changeDrawableColor(bgColor) - if (ageLevel != 1 && (ViewMap.TIMESTAMP.dynData?.stepColor ?: 0) == 0) + if (ageLevel != 1 && (ViewMap.TIMESTAMP.dynData?.stepFontColor ?: 0) == 0) binding.timestamp.setTextColor(ContextCompat.getColor(this, R.color.dark_TimestampOld)) - if (status.batteryLevel != 1 && (ViewMap.UPLOADER_BATTERY.dynData?.stepColor ?: 0) == 0) + if (status.batteryLevel != 1 && (ViewMap.UPLOADER_BATTERY.dynData?.stepFontColor ?: 0) == 0) binding.uploaderBattery.setTextColor(lowBatColor) if ((ViewMap.LOOP.dynData?.stepDraw ?: 0) == 0) // Apply automatic background image only if no dynData or no step images when (loopLevel) { @@ -431,7 +431,7 @@ class CustomWatchface : BaseWatchFace() { ); companion object { - + val TRANSPARENT = "#00000000" fun init(cwf: CustomWatchface) = values().forEach { it.cwf = cwf // reset all customized drawable when new watchface is loaded @@ -508,11 +508,19 @@ class CustomWatchface : BaseWatchFace() { FontMap.font(viewJson.optString(FONT.key, FontMap.DEFAULT.key)), StyleMap.style(viewJson.optString(FONTSTYLE.key, StyleMap.NORMAL.key)) ) - view.setTextColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(FONTCOLOR.key))) + view.setTextColor(dynData?.getFontColor() ?: cwf.getColor(viewJson.optString(FONTCOLOR.key))) view.isAllCaps = viewJson.optBoolean(ALLCAPS.key) if (viewJson.has(TEXTVALUE.key)) view.text = viewJson.optString(TEXTVALUE.key) - view.background = dynData?.getDrawable() ?: textDrawable() + (dynData?.getDrawable() ?: textDrawable())?.let { + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) not for svg files + it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + else + it.clearColorFilter() + view.background = it + } ?: apply { // if no drawable loaded either background key or dynData, then apply color to text background + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, TRANSPARENT), Color.TRANSPARENT)) + } } ?: apply { view.text = "" } } @@ -521,26 +529,35 @@ class CustomWatchface : BaseWatchFace() { view.clearColorFilter() viewJson?.let { viewJson -> drawable?.let { - if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) or xml included into res, not for svg files + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) not for svg files it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else it.clearColorFilter() view.setImageDrawable(it) } ?: apply { view.setImageDrawable(defaultDrawable?.let { cwf.resources.getDrawable(it) }) - if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // works on xml included into res files view.setColorFilter(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) else view.clearColorFilter() } + if (view.drawable == null) // if no drowable (either default, hardcoded or dynData, then apply color to background + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, TRANSPARENT), Color.TRANSPARENT)) } } fun customizeGraphView(view: lecho.lib.hellocharts.view.LineChartView) { customizeViewCommon(view) viewJson?.let { viewJson -> - view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, "#0000000000"), Color.TRANSPARENT)) - view.background = dynData?.getDrawable() ?: textDrawable() + (dynData?.getDrawable() ?: textDrawable())?.let { + if (viewJson.has(COLOR.key) || (dynData?.stepColor ?: 0) > 0) // Note only works on bitmap (png or jpg) not for svg files + it.colorFilter = cwf.changeDrawableColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key))) + else + it.clearColorFilter() + view.background = it + } ?: apply { // if no drowable loaded, then apply color to background + view.setBackgroundColor(dynData?.getColor() ?: cwf.getColor(viewJson.optString(COLOR.key, TRANSPARENT), Color.TRANSPARENT)) + } } } } @@ -554,7 +571,7 @@ class CustomWatchface : BaseWatchFace() { FLAT("\u2192", R.drawable.ic_flat, ResFileMap.ARROW_FLAT, 4.0), FORTY_FIVE_DOWN("\u2198", R.drawable.ic_fortyfivedown, ResFileMap.ARROW_FORTY_FIVE_DOWN, 3.0), SINGLE_DOWN("\u2193", R.drawable.ic_singledown, ResFileMap.ARROW_SINGLE_DOWN, 2.0), - DOUBLE_DOWN("\u21ca", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 2.0), + DOUBLE_DOWN("\u21ca", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 1.0), TRIPLE_DOWN("X", R.drawable.ic_doubledown, ResFileMap.ARROW_DOUBLE_DOWN, 1.0); companion object { @@ -695,6 +712,7 @@ class CustomWatchface : BaseWatchFace() { private val dynDrawable = mutableMapOf() private val dynColor = mutableMapOf() + private val dynFontColor = mutableMapOf() private var dataRange: DataRange? = null private var topRange: DataRange? = null private var leftRange: DataRange? = null @@ -703,6 +721,8 @@ class CustomWatchface : BaseWatchFace() { get() = dynDrawable.size - 1 val stepColor: Int get() = dynColor.size - 1 + val stepFontColor: Int + get() = dynFontColor.size - 1 val dataValue: Double? get() = when (valueMap) { @@ -727,6 +747,7 @@ class CustomWatchface : BaseWatchFace() { ?: (leftRange.invalidData * cwf.zoomFactor).toInt() } } ?: 0 fun getRotationOffset(): Int = dataRange?.let { dataRange -> rotationRange?.let { rotRange -> dataValue?.let { valueMap.dynValue(it, dataRange, rotRange) } ?: rotRange.invalidData } } ?: 0 fun getDrawable() = dataRange?.let { dataRange -> dataValue?.let { dynDrawable[valueMap.stepValue(it, dataRange, stepDraw)] } ?: dynDrawable[0] } + fun getFontColor() = if (stepFontColor > 0) dataRange?.let { dataRange -> dataValue?.let { dynFontColor[valueMap.stepValue(it, dataRange, stepFontColor)] } ?: dynFontColor[0] } else null fun getColor() = if (stepColor > 0) dataRange?.let { dataRange -> dataValue?.let { dynColor[valueMap.stepValue(it, dataRange, stepColor)] } ?: dynColor[0] } else null private fun load() { dynDrawable[0] = dataJson.optString(INVALIDIMAGE.key)?.let { cwf.resDataMap[it]?.toDrawable(cwf.resources, width, height) } @@ -741,6 +762,12 @@ class CustomWatchface : BaseWatchFace() { dynColor[idx] = cwf.getColor(dataJson.optString("${COLOR.key}$idx")) idx++ } + dynFontColor[0] = cwf.getColor(dataJson.optString(INVALIDFONTCOLOR.key)) + idx = 1 + while (dataJson.has("${FONTCOLOR.key}$idx")) { + dynFontColor[idx] = cwf.getColor(dataJson.optString("${FONTCOLOR.key}$idx")) + idx++ + } DataRange(dataJson.optDouble(MINDATA.key, valueMap.min), dataJson.optDouble(MAXDATA.key, valueMap.max)).let { defaultRange -> dataRange = defaultRange topRange = parseDataRange(dataJson.optJSONObject(TOPOFFSET.key), defaultRange) diff --git a/wear/src/main/res/values-nb-rNO/strings.xml b/wear/src/main/res/values-nb-rNO/strings.xml index 426e56cc75..08c0405bf7 100644 --- a/wear/src/main/res/values-nb-rNO/strings.xml +++ b/wear/src/main/res/values-nb-rNO/strings.xml @@ -36,7 +36,7 @@ Vis Gj.snittDelta Vis telefonbatteri Vis riggens batteri - Vis basalrate + Vis basaldose Vis loop status Vis BS Vis BGI @@ -109,7 +109,7 @@ eKarbo Prosent Start [min] - Varighet [h] + Varighet [t] Insulin Forhåndsinnstilling 1 Forhåndsinnstilling 2