Merge branch 'dev' into nscv3service
This commit is contained in:
commit
970923df03
81 changed files with 5522 additions and 292 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -140,7 +140,7 @@ public abstract class BaseSeries<E extends DataPointInterface> 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();
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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<PrefsFile>
|
||||
fun listCustomWatchfaceFiles(): MutableList<CwfData>
|
||||
fun listCustomWatchfaceFiles(): MutableList<CwfFile>
|
||||
fun checkMetadata(metadata: Map<PrefsMetadataKey, PrefMetadata>): Map<PrefsMetadataKey, PrefMetadata>
|
||||
fun formatExportedAgo(utcTime: String): String
|
||||
}
|
|
@ -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()
|
||||
class EventMobileDataToWear(val payload: ByteArray) : Event()
|
|
@ -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<String, ResData>
|
||||
typealias CwfMetadataMap = MutableMap<CwfMetadataKey, String>
|
||||
fun CwfResDataMap.isEquals(dataMap: CwfResDataMap) = (this.size == dataMap.size) && this.all { (key, resData) -> dataMap[key]?.value.contentEquals(resData.value) == true }
|
||||
|
||||
@Serializable
|
||||
data class CwfData(val json: String, var metadata: CwfMetadataMap, val resDatas: CwfResDataMap)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<string name="pref_show_avgdelta">Vis Gj. snitt Delta</string>
|
||||
<string name="pref_show_phone_battery">Vis telefonbatteri</string>
|
||||
<string name="pref_show_rig_battery">Vis riggens batteri</string>
|
||||
<string name="pref_show_basal_rate">Vis basalrate</string>
|
||||
<string name="pref_show_basal_rate">Vis basaldose</string>
|
||||
<string name="pref_show_loop_status">Vis loop status</string>
|
||||
<string name="pref_show_bg">Vis BS</string>
|
||||
<string name="pref_show_bgi">Vis BGI</string>
|
||||
|
@ -75,7 +75,7 @@
|
|||
<string name="cwf_comment_avg_delta">Gj.snitt BS-endring (15min)</string>
|
||||
<string name="cwf_comment_uploader_battery">Telefonbatteri (%)</string>
|
||||
<string name="cwf_comment_rig_battery">Rig-batteri (%)</string>
|
||||
<string name="cwf_comment_basalRate">Basalrate</string>
|
||||
<string name="cwf_comment_basalRate">Basaldose</string>
|
||||
<string name="cwf_comment_bgi">BGI verdi</string>
|
||||
<string name="cwf_comment_time">Tid (TT:MM eller TT:MM:SS)</string>
|
||||
<string name="cwf_comment_hour">Time (TT)</string>
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
<string name="dia_long_label">Insulinets virkningstid (DIA)</string>
|
||||
<string name="ic_long_label">Insulin-karbohydratfaktor (IK)</string>
|
||||
<string name="isf_long_label">Insulin sensitivitetsfaktor (ISF)</string>
|
||||
<string name="basal_long_label">Basalrate</string>
|
||||
<string name="basal_long_label">Basaldose</string>
|
||||
<string name="target_long_label">Blodsukkermål</string>
|
||||
<string name="shortgram">g</string>
|
||||
<string name="shortpercent">%</string>
|
||||
|
@ -193,7 +193,7 @@
|
|||
<!-- Temptarget-->
|
||||
<string name="mins">%1$d min</string>
|
||||
<!-- Translator-->
|
||||
<string name="careportal">Careportal</string>
|
||||
<string name="careportal">Helseportal</string>
|
||||
<string name="careportal_bgcheck">BS-kontroll</string>
|
||||
<string name="careportal_mbg">Manuelt BS eller kalibrering</string>
|
||||
<string name="careportal_announcement">Melding</string>
|
||||
|
@ -294,19 +294,19 @@
|
|||
<string name="uel_cancel_bolus">AVBRYT BOLUS</string>
|
||||
<string name="uel_cancel_extended_bolus">AVBRYT FORLENGET BOLUS</string>
|
||||
<string name="uel_cancel_tt">AVBRYT MIDL. MÅL</string>
|
||||
<string name="uel_careportal">CAREPORTAL</string>
|
||||
<string name="uel_careportal">HELSEPORTAL</string>
|
||||
<string name="uel_site_change">BYTTE SLANGESETT</string>
|
||||
<string name="uel_reservoir_change">BYTTE RESERVOAR</string>
|
||||
<string name="uel_calibration">KALIBRERING</string>
|
||||
<string name="uel_prime_bolus">PRIME BOLUS</string>
|
||||
<string name="uel_treatment">BEHANDLING</string>
|
||||
<string name="uel_careportal_ns_refresh">CAREPORTAL NS OPPDATERING</string>
|
||||
<string name="uel_careportal_ns_refresh">HELSEPORTAL NS-OPPDATERING</string>
|
||||
<string name="uel_profile_switch_ns_refresh">PROFILBYTTE NS OPPDATERING</string>
|
||||
<string name="uel_treatments_ns_refresh">BEHANDLINGER NS OPPDATERING</string>
|
||||
<string name="uel_tt_ns_refresh">OPPDATER MIDL. MÅL NS</string>
|
||||
<string name="uel_automation_removed">AUTOMASJON FJERNET</string>
|
||||
<string name="uel_bg_removed">BS FJERNET</string>
|
||||
<string name="uel_careportal_removed">CAREPORTAL FJERNET</string>
|
||||
<string name="uel_careportal_removed">HELSEPORTAL FJERNET</string>
|
||||
<string name="uel_bolus_removed">BOLUS FJERNET</string>
|
||||
<string name="uel_carbs_removed">KARBO FJERNET</string>
|
||||
<string name="uel_temp_basal_removed">MIDL. MÅL FJERNET</string>
|
||||
|
@ -486,7 +486,7 @@
|
|||
<string name="resistant_adult">Insulinresistent voksen</string>
|
||||
<string name="pregnant">Graviditet</string>
|
||||
<string name="patient_age_summary">Velg pasienttype for oppsett av sikkerhetsgrenser</string>
|
||||
<string name="max_bolus_title">Maks tillatt bolus [U]</string>
|
||||
<string name="max_bolus_title">Maks tillatt bolus [E]</string>
|
||||
<string name="max_carbs_title">Maks tillatt karbohydrater [g]</string>
|
||||
<string name="patient_type">Pasienttype</string>
|
||||
<!-- Protection-->
|
||||
|
@ -561,9 +561,9 @@
|
|||
<!-- SmsCommunicator -->
|
||||
<string name="smscommunicator_missingsmspermission">Mangler SMS-tillatelse</string>
|
||||
<!-- About -->
|
||||
<string name="cta_dont_kill_my_app_info">Hvordan hindre at appen stenges?</string>
|
||||
<string name="cta_dont_kill_my_app_info">Ikke steng appen min?</string>
|
||||
<string name="fabric_upload_disabled">Opplast av krasjlogger er deaktivert!</string>
|
||||
<string name="about_link_urls">\n\nDokumentasjon:\nhttps://androidaps.readthedocs.io\n\nfacebook:\nhttps://www.facebook.com/groups/AndroidAPSUsers</string>
|
||||
<string name="about_link_urls">\n\nDokumentasjon:\nhttps://androidaps.readthedocs.io\n\nFacebook:\nhttps://www.facebook.com/groups/AndroidAPSUsers</string>
|
||||
<plurals name="days">
|
||||
<item quantity="one">%1$d dag</item>
|
||||
<item quantity="other">%1$d dager</item>
|
||||
|
|
|
@ -116,6 +116,9 @@
|
|||
<string name="key_wearwizard_cob" translatable="false">wearwizard_cob</string>
|
||||
<string name="key_wearwizard_iob" translatable="false">wearwizard_iob</string>
|
||||
<string name="key_wear_custom_watchface_autorization" translatable="false">wear_custom_watchface_autorization</string>
|
||||
<string name="key_wear_cwf_watchface_name" translatable="false">wear_cwf_watchface_name</string>
|
||||
<string name="key_wear_cwf_author_version" translatable="false">wear_cwf_author_version</string>
|
||||
<string name="key_wear_cwf_filename" translatable="false">wear_cwf_filename</string>
|
||||
<string name="key_objectives_bg_is_available_in_ns" translatable="false">ObjectivesbgIsAvailableInNS</string>
|
||||
<string name="key_objectives_pump_status_is_available_in_ns" translatable="false">ObjectivespumpStatusIsAvailableInNS</string>
|
||||
<string name="key_statuslights_cage_warning" translatable="false">statuslights_cage_warning</string>
|
||||
|
|
|
@ -187,7 +187,9 @@ data class UserEntry(
|
|||
Overview, //From OverViewPlugin
|
||||
Stats, //From Stat Activity
|
||||
Aaps, // MainApp
|
||||
GarminDevice,
|
||||
Unknown //if necessary
|
||||
,
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
|
3605
database/impl/schemas/app.aaps.database.impl.AppDatabase/22.json
Normal file
3605
database/impl/schemas/app.aaps.database.impl.AppDatabase/22.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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<Context>()
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<HeartRate>, val updated: List<HeartRate>)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -41,8 +41,8 @@
|
|||
<string name="openapsama_use_autosens">Bruk Autosens-funksjon</string>
|
||||
<string name="openapsma_max_basal_title">Max E/t en midl. basal kan settes til</string>
|
||||
<string name="openapsma_max_basal_summary">Denne verdien kalles max basal i OpenAPS</string>
|
||||
<string name="openapsma_max_iob_title">Maksimum basal IOB som OpenAPS kan levere [U]</string>
|
||||
<string name="openapsma_max_iob_summary">Denne verdien kalles Max IOB i OpenAPS.\nDet er maks insulinmengde i [U] som APS kan levere i en dose.</string>
|
||||
<string name="openapsma_max_iob_title">Maksimum basal IOB som OpenAPS kan levere [E]</string>
|
||||
<string name="openapsma_max_iob_summary">Denne verdien kalles Max IOB i OpenAPS.\nDet er maks insulinmengde i [E] som APS kan levere i en dose.</string>
|
||||
<string name="openapsama_autosens_adjust_targets_summary">Standard verdi: sann\nGir autosens tillatelse til å justere BS-mål, i tillegg til ISF og basaler.</string>
|
||||
<string name="openapsama_autosens_adjust_targets">Autosens justerer også BS målverdier</string>
|
||||
<string name="openapsama_min_5m_carb_impact_summary">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.</string>
|
||||
|
@ -54,8 +54,8 @@
|
|||
<string name="always_use_short_avg_summary">Nyttig når data fra ufiltrerte kilder som xDrip+ registrerer mye støy.</string>
|
||||
<string name="openapsama_max_daily_safety_multiplier">Multiplikator for maks daglig basal</string>
|
||||
<string name="openapsama_current_basal_safety_multiplier">Multiplikator for gjeldende basal</string>
|
||||
<string name="openapssmb_max_iob_title">Maks total IOB OpenAPS ikke kan overstige [U]</string>
|
||||
<string name="openapssmb_max_iob_summary">Denne verdien kalles Maks IOB av OpenAPS\nOpenAPS vil ikke gi mere insulin hvis mengden insulin ombord (IOB) overstiger denne verdien</string>
|
||||
<string name="openapssmb_max_iob_title">Maks total IOB OpenAPS ikke kan overstige [E]</string>
|
||||
<string name="openapssmb_max_iob_summary">Denne verdien kalles Maks IOB av OpenAPS\nAAPS vil ikke gi mere insulin hvis mengden insulin ombord (IOB) overstiger denne verdien</string>
|
||||
<string name="enable_uam">Aktiver UAM</string>
|
||||
<string name="enable_smb">Aktiver SMB</string>
|
||||
<string name="enable_smb_summary">Bruk Supermikrobolus i stedet for midl. basal for raskere resultat</string>
|
||||
|
@ -65,7 +65,7 @@
|
|||
<string name="enable_smb_after_carbs">Aktiver SMB etter karbohydrater</string>
|
||||
<string name="enable_smb_after_carbs_summary">Aktiver SMB i 6t etter karbohydratinntak, selv med 0 COB. Bare mulig med en bra filtrert BS kilde som f. eks. Dexcom G5/G6</string>
|
||||
<string name="enable_smb_with_cob">Aktiver SMB med COB</string>
|
||||
<string name="enable_smb_with_cob_summary">Aktiver SMB når COB er aktiv.</string>
|
||||
<string name="enable_smb_with_cob_summary">Aktiver SMB når COB (karbohydrater ombord) er aktiv.</string>
|
||||
<string name="enable_smb_with_temp_target">Aktiver SMB med midl. målverdi</string>
|
||||
<string name="enable_smb_with_temp_target_summary">Aktiver SMB når midl. målverdi er aktivert (spise snart, trening)</string>
|
||||
<string name="enable_smb_with_high_temp_target">Aktiver SMB ved høy midl. målverdi</string>
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
<string name="cobcompared">COB %1$s %2$.0f</string>
|
||||
<string name="triggerHeartRate">Puls</string>
|
||||
<string name="triggerHeartRateDesc">HR %1$s %2$.0f</string>
|
||||
<string name="iob_u">IOB [U]:</string>
|
||||
<string name="iob_u">IOB [E]:</string>
|
||||
<string name="distance_short">Dist [m]:</string>
|
||||
<string name="recurringTime">Gjentakende tidspunkt</string>
|
||||
<string name="every">Hver</string>
|
||||
|
|
BIN
plugins/configuration/src/main/assets/Analog G-Watch.zip
Normal file
BIN
plugins/configuration/src/main/assets/Analog G-Watch.zip
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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<CwfData> {
|
||||
val customWatchfaceFiles = mutableListOf<CwfData>()
|
||||
val customAwtchfaceAuthorization = sp.getBoolean(app.aaps.core.utils.R.string.key_wear_custom_watchface_autorization, false)
|
||||
override fun listCustomWatchfaceFiles(): MutableList<CwfFile> {
|
||||
val customWatchfaceFiles = mutableListOf<CwfFile>()
|
||||
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) {
|
||||
|
|
|
@ -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<CwfData>) : RecyclerView.Adapter<RecyclerViewAdapter.CwfFileViewHolder>() {
|
||||
inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List<CwfFile>) : RecyclerView.Adapter<RecyclerViewAdapter.CwfFileViewHolder>() {
|
||||
|
||||
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)
|
||||
|
|
|
@ -117,12 +117,12 @@
|
|||
<string name="database_cleanup">Databaseopprydding</string>
|
||||
<string name="reset_db_confirm">Vil du virkelig nullstille databasene?</string>
|
||||
<string name="maintenance_settings">Vedlikeholdsinnstillinger</string>
|
||||
<string name="maintenance_email">E-post mottaker</string>
|
||||
<string name="maintenance_email">Mottaker av e-post</string>
|
||||
<string name="maintenance_amount">Antall logger du vil sende</string>
|
||||
<string name="send_all_logs">Send logger via e-post</string>
|
||||
<string name="delete_logs">Slett logger</string>
|
||||
<string name="configbuilder_nightscoutversion_label">Nightscout versjon:</string>
|
||||
<string name="engineering_mode_enabled">Engineering Mode aktivert</string>
|
||||
<string name="configbuilder_nightscoutversion_label">Nightscout-versjon:</string>
|
||||
<string name="engineering_mode_enabled">Engineering mode aktivert</string>
|
||||
<string name="log_files">Loggfiler</string>
|
||||
<string name="nav_logsettings">Logginnstillinger</string>
|
||||
<string name="miscellaneous">Annet</string>
|
||||
|
|
|
@ -167,13 +167,13 @@
|
|||
<string name="extendedcarbs_rescue">For å registrere karbohydrater som brukes til å korrigere lavt blodsukker.</string>
|
||||
<string name="extendedcarbs_hint1">https://wiki.aaps.app/en/latest/Usage/Extended-Carbs.html</string>
|
||||
<string name="nsclient_label">Fjernovervåking</string>
|
||||
<string name="nsclient_howcanyou">Hvordan kan du overvåke AndroidAPS (for eksempel for ditt barn) på eksternt?</string>
|
||||
<string name="nsclient_nightscout">AAPSClient app, Nightscout app og Nightscout websiden gjør det mulig for deg å følge AAPS eksternt.</string>
|
||||
<string name="nsclient_howcanyou">Hvordan kan du eksternt overvåke AndroidAPS (for eksempel for ditt barn)?</string>
|
||||
<string name="nsclient_nightscout">Appene AAPSClient og xDrip+ samt Nightscout-websiden gjør det mulig for deg å følge AAPS eksternt.</string>
|
||||
<string name="nsclient_dexcomfollow">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.</string>
|
||||
<string name="nsclient_data">For å følge AAPS eksternt må begge enhetene ha Internett-tilgang (f.eks. via Wi-Fi eller mobildata).</string>
|
||||
<string name="nsclient_fullcontrol">AAPSClient som brukes som ekstern følger app vil både overvåke og gi full kontroll over AAPS.</string>
|
||||
<string name="nsclient_hint1">https://wiki.aaps.app/en/latest/Children/Children.html</string>
|
||||
<string name="isf_label_exam">Insulin Sensitivitetsfaktor (ISF)</string>
|
||||
<string name="isf_label_exam">Insulinsensitivitetsfaktor (ISF)</string>
|
||||
<string name="isf_increasingvalue">Økte ISF-verdiene vil føre til levering av mer insulin for å dekke opp en viss mengde karbohydrater.</string>
|
||||
<string name="isf_decreasingvalue">Redusering av ISF verdien vil føre til mer insulintilførsel for å korrigere et blodsukker som ligger over målverdien.</string>
|
||||
<string name="isf_noeffect">Å øke eller senke ISF verdien har ingen effekt på insulintilførselen når blodsukkeret er lavere enn målverdien.</string>
|
||||
|
@ -211,7 +211,7 @@
|
|||
<string name="profileswitchtime_100">Gjør et profilbytte til mer enn 100%.</string>
|
||||
<string name="profileswitchtime_hint1">https://wiki.aaps.app/en/latest/Usage/Profiles.html#timeshift</string>
|
||||
<string name="profileswitch4_label">Endring av profil</string>
|
||||
<string name="profileswitch4_rates">Basalrater, ISF, KH ratio, etc., bør defineres i profiler.</string>
|
||||
<string name="profileswitch4_rates">Basaldoser, ISF, IK-faktor osv. bør defineres i profiler.</string>
|
||||
<string name="profileswitch4_internet">Aktivering av endringer i Nightscout profilen din krever at din AndroidAPS telefon er koblet til Internett.</string>
|
||||
<string name="profileswitch4_sufficient">Å redigere verdier i profilen din er tilstrekkelig for å aktivere profilendringen.</string>
|
||||
<string name="profileswitch4_multi">Flere profiler kan defineres og velges for å håndtere endringer i omstendigheter (f.eks. hormonelle endringer, skift arbeid, hverdager/helgedager).</string>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<string name="objectives_autosens_gate">1 uke vellykket looping på dagtid hvor alle måltider (KH) angis</string>
|
||||
<string name="objectives_autosens_learned">Hvis dine autosens resultater ikke ligger rundt 100% kan det tyde på at profilen din er feil.</string>
|
||||
<string name="objectives_smb_objective">Aktiver ekstra funksjoner for bruk på dagtid, slik som SMB (Super Micro Bolus)</string>
|
||||
<string name="objectives_smb_gate">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</string>
|
||||
<string name="objectives_smb_gate">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</string>
|
||||
<string name="objectives_smb_learned">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?</string>
|
||||
<string name="objectives_dyn_isf_objective">Aktivere ekstra funksjoner for bruk på dagtid, slik som Dynamisk Sensitivitet</string>
|
||||
<string name="objectives_dyn_isf_gate">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.</string>
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".general.wear.activities.CwfInfosActivity"
|
||||
android:name="app.aaps.plugins.sync.wear.activities.CwfInfosActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
|||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".general.wear.wearintegration.DataLayerListenerServiceMobile"
|
||||
android:name="app.aaps.plugins.sync.wear.wearintegration.DataLayerListenerServiceMobile"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.gms.wearable.CHANNEL_EVENT" />
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -156,7 +156,7 @@
|
|||
<!-- Actions -->
|
||||
<string name="actions">Handlinger</string>
|
||||
<string name="description_actions">Hurtigknapper for rask tilgang til ofte brukte funksjoner</string>
|
||||
<string name="actions_shortname">ACT</string>
|
||||
<string name="actions_shortname">HAN</string>
|
||||
<string name="tempbasal_button">Midlertidig basal</string>
|
||||
<string name="extended_bolus_button">Forlenget bolus</string>
|
||||
<string name="extended_bolus_cancel_button">Avbryt forlenget bolus</string>
|
||||
|
@ -174,18 +174,18 @@
|
|||
<string name="patch_pump">Pumpe</string>
|
||||
<!-- Overview -->
|
||||
<string name="show_statuslights">Vis statusindikatorer på hjem-skjermen</string>
|
||||
<string name="statuslights_cage_warning">Terskel for advarsel om alder på slangesett [h]</string>
|
||||
<string name="statuslights_cage_critical">Terskel for kritisk alder på slangesett [h]</string>
|
||||
<string name="statuslights_iage_warning">Terskel for advarsel, alder på insulin [h]</string>
|
||||
<string name="statuslights_iage_critical">Terskel for kritisk alder på insulin [h]</string>
|
||||
<string name="statuslights_sage_warning">Terskel for advarsel, alder på CGM [h]</string>
|
||||
<string name="statuslights_sage_critical">Terskel for kritisk alder på CGM [h]</string>
|
||||
<string name="statuslights_cage_warning">Terskel for advarsel om alder på slangesett [t]</string>
|
||||
<string name="statuslights_cage_critical">Terskel for kritisk alder på slangesett [t]</string>
|
||||
<string name="statuslights_iage_warning">Terskel for advarsel, alder på insulin [t]</string>
|
||||
<string name="statuslights_iage_critical">Terskel for kritisk alder på insulin [t]</string>
|
||||
<string name="statuslights_sage_warning">Terskel for advarsel, alder på sensor [t]</string>
|
||||
<string name="statuslights_sage_critical">Terskel for kritisk alder på sensor [t]</string>
|
||||
<string name="statuslights_sbat_warning">Terskel for advarsel, batterinivå for sensor [%]</string>
|
||||
<string name="statuslights_sbat_critical">Terskel for kritisk batterinivå for sensor [%]</string>
|
||||
<string name="statuslights_bage_warning">Terskel for advarsel, batterialder for pumpe [h]</string>
|
||||
<string name="statuslights_bage_critical">Terskel for kritisk batterialder for pumpe [h]</string>
|
||||
<string name="statuslights_res_warning">Terskel for advarsel, insulinreservoar [U]</string>
|
||||
<string name="statuslights_res_critical">Terskel for kritisk insulinreservoar [U]</string>
|
||||
<string name="statuslights_bage_warning">Terskel for advarsel, batterialder for pumpe [t]</string>
|
||||
<string name="statuslights_bage_critical">Terskel for kritisk batterialder for pumpe [t]</string>
|
||||
<string name="statuslights_res_warning">Terskel for advarsel, insulinreservoar [E]</string>
|
||||
<string name="statuslights_res_critical">Terskel for kritisk insulinreservoar [E]</string>
|
||||
<string name="statuslights_bat_warning">Terskel for advarsel, batterinivå for pumpe [%]</string>
|
||||
<string name="statuslights_bat_critical">Terskel for kritisk batterinivå for pumpe [%]</string>
|
||||
<string name="statuslights_copy_ns">Kopier innstillingene fra NS</string>
|
||||
|
@ -242,11 +242,11 @@
|
|||
<string name="high_mark">Høy verdi</string>
|
||||
<string name="short_tabtitles">Korte navn i menyfaner</string>
|
||||
<string name="overview_show_notes_field_in_dialogs_title">Vis merknadsfelt i dialogvindu for boluskalkulator</string>
|
||||
<string name="deliverpartofboluswizard">Bolusveiviser utfører beregninger, men bare denne del av beregnet insulin leveres. Nyttig ved bruk av SMB-algoritmen.</string>
|
||||
<string name="deliverpartofboluswizard">Boluskalkulator utfører beregninger, men bare denne del av beregnet insulin leveres. Nyttig ved bruk av SMB-algoritmen.</string>
|
||||
<string name="deliver_part_of_boluswizard_reset_time">Gi full bolus (100 %) dersom blodsukker er eldre enn</string>
|
||||
<string name="enable_bolus_advisor">Aktiver bolusveileder</string>
|
||||
<string name="enable_bolus_advisor_summary">Bruk en påminnelse om å spise senere istedet for boluskalkulatorens resultat når blodsukker er høyt (\"pre-bolus\")</string>
|
||||
<string name="enablesuperbolus">Aktiver superbolus i veiviser</string>
|
||||
<string name="enablesuperbolus">Aktiver superbolus i boluskalkulator</string>
|
||||
<string name="enablesuperbolus_summary">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!</string>
|
||||
<string name="enablebolusreminder">Aktiver boluspåminnelse</string>
|
||||
<string name="enablebolusreminder_summary">Bruk en påminnelse for å sette bolusdosen senere med boluskalkulatoren («post bolus»)</string>
|
||||
|
|
|
@ -330,76 +330,9 @@
|
|||
<string name="send_logfiles">Send today\'s log files to developers along with this time. Unexpected situation.</string>
|
||||
<string name="iob_cob_calculator" translatable="false">IobCobCalculator</string>
|
||||
|
||||
<!-- Wear-->
|
||||
<string name="wear_shortname">WEAR</string>
|
||||
<string name="description_wear">Monitor and control AAPS using your WearOS watch.</string>
|
||||
<string name="no_watch_connected">(No Watch Connected)</string>
|
||||
<string name="pump_status">Pump status</string>
|
||||
<string name="loop_status">Loop status</string>
|
||||
<string name="wizard_result">Calc. Wizard:\nInsulin: %1$.2fU\nCarbs: %2$dg</string>
|
||||
<string name="quick_wizard_not_available">Selected quickwizard no longer available, please refresh your tile</string>
|
||||
<string name="quick_wizard_message">QuickWizard: %1$s\nInsulin: %2$.2fU\nCarbs: %3$dg</string>
|
||||
<string name="wear_action_tempt_preset_error">Temptarget unknown preset: %1$s</string>
|
||||
<string name="wear_action_tempt_cancel_message">Cancelling running Temp-Targets?</string>
|
||||
<string name="wear_action_tempt_unit_error">Different units used on watch and phone!</string>
|
||||
<string name="wear_action_tempt_zero_message">Zero-Temp-Target - cancelling running Temp-Targets?</string>
|
||||
<string name="wear_action_tempt_min_bg_error">Min-BG out of range!</string>
|
||||
<string name="wear_action_tempt_max_bg_error">Max-BG out of range!</string>
|
||||
<string name="wear_action_tempt_manual_range_message">Temptarget:\nMin: %1$s\nMax: %2$s\nDuration: %3$s</string>
|
||||
<string name="wear_action_tempt_manual_message">Temptarget:\nTarget: %1$s\nDuration: %2$s</string>
|
||||
<string name="wear_action_tempt_preset_message">Temptarget:\nReason: %1$s\nTarget: %2$s\nDuration: %3$s</string>
|
||||
<string name="no_success">not successful - please check phone</string>
|
||||
<string name="wear_settings">Wear settings</string>
|
||||
<string name="wearcontrol_title">Controls from Watch</string>
|
||||
<string name="wearcontrol_summary">Set Temp-Targets and enter Treatments from the watch.</string>
|
||||
<string name="wear_wizard_settings_summary">Calculations included in the Wizard result:</string>
|
||||
<string name="wear_general_settings">General Settings</string>
|
||||
<string name="wear_notifysmb_title">Notify on SMB</string>
|
||||
<string name="wear_notifysmb_summary">Show SMB on the watch like a standard bolus.</string>
|
||||
<string name="wear_custom_watchface_settings">Custom Watchface Settings</string>
|
||||
<string name="wear_custom_watchface_authorization_title">Custom Watchface Authorization</string>
|
||||
<string name="wear_custom_watchface_authorization_summary">Authorize loaded custom watchface to change and lock some watch display settings to suit watchface design</string>
|
||||
<string name="wear_custom_watchface">Custom Watchface: %1$s</string>
|
||||
<string name="wear_load_watchface">Load Watchface</string>
|
||||
<string name="wear_infos_watchface">Infos Watchface</string>
|
||||
<string name="wear_export_watchface">Export template</string>
|
||||
<string name="wear_new_custom_watchface_exported">Custom watchface template exported</string>
|
||||
<string name="resend_all_data">Resend All Data</string>
|
||||
<string name="open_settings_on_wear">Open Settings on Wear</string>
|
||||
<string name="cwf_infos_pref_locked">List of prefs locked by the Watchface</string>
|
||||
<string name="cwf_infos_pref_required">List of prefs required for the Watchface</string>
|
||||
<string name="cwf_infos_view_title">List of fields included into the Watchface</string>
|
||||
|
||||
|
||||
<!-- PersistentNotification-->
|
||||
<string name="ongoingnotificaction" translatable="false">Ongoing Notification</string>
|
||||
<string name="description_persistent_notification">Shows an ongoing notification with a short overview of what your loop is doing</string>
|
||||
<string name="old_data">OLD DATA</string>
|
||||
<string name="pump_fetching_data">trying to fetch data from pump.</string>
|
||||
<string name="pump_old_data">TDD: Still old data! Cannot load from pump.</string>
|
||||
<string name="grams_short">g</string>
|
||||
<string name="hour_short">h</string>
|
||||
<string name="no_active_profile">No active profile switch!</string>
|
||||
<string name="profile_message">Profile:\n\nTimeshift: %1$d\nPercentage: %2$d%%\"</string>
|
||||
<string name="tdd_line">%1$.2fU %1$.0f%%</string>
|
||||
<string name="no_profile">No profile loaded</string>
|
||||
<string name="aps_only">Only apply in APS mode!</string>
|
||||
<string name="last_aps_result_na">Last result not available!</string>
|
||||
<string name="loop_status_closed">CLOSED LOOP</string>
|
||||
<string name="loop_status_open">OPEN LOOP</string>
|
||||
<string name="loop_status_disabled">LOOP DISABLED</string>
|
||||
<string name="aps">APS</string>
|
||||
<string name="last_run">Last run</string>
|
||||
<string name="last_enact">Last Enact</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="weighted">weighted</string>
|
||||
<string name="target_only_aps_mode">Targets only apply in APS mode!</string>
|
||||
<string name="no_history">No history data!</string>
|
||||
<string name="units_short">U</string>
|
||||
<string name="temp_target">Temp Target</string>
|
||||
<string name="until">until</string>
|
||||
<string name="default_range">DEFAULT RANGE</string>
|
||||
<string name="target">target</string>
|
||||
<string name="rate_duration">Rate: %1$.2fU/h (%2$.2f%%) \nDuration %3$d min</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -9,15 +9,15 @@
|
|||
<string name="description_sensitivity_weighted_average">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.</string>
|
||||
<string name="uam_disabled_oref1_not_selected">UAM deaktivert fordi den trenger Oref1 sensitivitetsplugin</string>
|
||||
<string name="absorption_settings_title">Absorberingsinnstillinger</string>
|
||||
<string name="absorption_max_time_title">Maks absorberingstid for måltid [h]</string>
|
||||
<string name="absorption_max_time_title">Maks absorberingstid for måltid [t]</string>
|
||||
<string name="absorption_max_time_summary">Tid i timer hvor det forventes at alle karbohydrater fra måltid vil være absorbert</string>
|
||||
<string name="openapsama_autosens_period">Intervall for autosens [h]</string>
|
||||
<string name="openapsama_autosens_period">Intervall for autosens [t]</string>
|
||||
<string name="openapsama_autosens_period_summary">Antall timer med historiske data for beregning av sensitivitet (absorpsjonstid for KH er ekskludert)</string>
|
||||
<string name="openapsama_autosens_max_summary">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.</string>
|
||||
<string name="openapsama_autosens_min_summary">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%.</string>
|
||||
<string name="openapsama_autosens_max">Maks autosens ratio</string>
|
||||
<string name="openapsama_autosens_min">Minimum autosens ratio</string>
|
||||
<string name="openapsama_min_5m_carb_impact_summary">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.</string>
|
||||
<string name="absorption_cutoff_title">Maks absorpsjonstid for måltid [h]</string>
|
||||
<string name="absorption_cutoff_title">Maks absorpsjonstid for måltid [t]</string>
|
||||
<string name="absorption_cutoff_summary">Etter denne tiden forventes det at måltidet er absorbert. Eventuelle gjenværende karbo vil tas ut av beregninger.</string>
|
||||
</resources>
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Long> {
|
||||
val byteBuffer: ByteBuffer = ByteBuffer.wrap(data)
|
||||
byteBuffer.order(ByteOrder.LITTLE_ENDIAN)
|
||||
byteBuffer.limit(toLongBoundary(end))
|
||||
val buffer: LongBuffer = byteBuffer.asLongBuffer()
|
||||
val encodedData: MutableList<Long> = 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GlucoseValue> {
|
||||
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<GlucoseValue> {
|
||||
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<GlucoseValue>): 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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, (SocketAddress, URI, String?) -> 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<URI, String?> {
|
||||
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<String, String?> = 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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GlucoseValue>
|
||||
|
||||
/** Stores hear rate readings that a taken and averaged of the given interval. */
|
||||
fun storeHeartRate(
|
||||
samplingStart: Instant, samplingEnd: Instant,
|
||||
avgHeartRate: Int,
|
||||
device: String?
|
||||
)
|
||||
}
|
|
@ -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<EffectiveProfileSwitch>)?.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<GlucoseValue> {
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Pair<ViewKeys, Boolean>>()
|
||||
|
||||
for (viewKey in ViewKeys.values()) {
|
||||
for (viewKey in ViewKeys.entries) {
|
||||
try {
|
||||
val jsonValue = json.optJSONObject(viewKey.key)
|
||||
if (jsonValue != null) {
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
|
@ -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
|
|
@ -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">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
|
@ -58,7 +58,8 @@
|
|||
android:text="Author: Name"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?attr/importListFileNameColor"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="18sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/created_at"
|
||||
|
@ -71,7 +72,8 @@
|
|||
android:text="created at: "
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?attr/importListFileNameColor"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="18sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cwf_version"
|
||||
|
@ -85,7 +87,8 @@
|
|||
android:textAlignment="viewStart"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?attr/importListFileNameColor"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="18sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cwf_comment"
|
||||
|
@ -99,7 +102,8 @@
|
|||
android:textAlignment="viewStart"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?attr/importListFileNameColor"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="18sp"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/pref_layout"
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/pref_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="0dp"
|
|
@ -3,7 +3,6 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/careportal_cardview"
|
||||
style="@style/Widget.MaterialComponents.CardView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -43,7 +42,7 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/view_comment"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="3dp"
|
||||
android:layout_marginTop="0dp"
|
|
@ -5,7 +5,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="2dp"
|
||||
tools:context=".general.wear.WearFragment">
|
||||
tools:context="app.aaps.plugins.sync.wear.WearFragment">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/log"
|
|
@ -27,7 +27,7 @@
|
|||
<string name="boluses">Boluser</string>
|
||||
<string name="extended_boluses">Forlenget bolus</string>
|
||||
<string name="carbohydrates">Karbohydrater</string>
|
||||
<string name="careportal_events">Careportal hendelser (unntatt notater)</string>
|
||||
<string name="careportal_events">Helseportal-hendelser (unntatt notater)</string>
|
||||
<string name="profile_switches">Profilbytter</string>
|
||||
<string name="total_daily_doses">Totale daglige doser</string>
|
||||
<string name="temporary_basal_rates">Midlertidige basal doser</string>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<string name="ns_allow_roaming">Tillat tilkobling i roaming</string>
|
||||
<string name="ns_create_announcements_from_errors_title">Lag meldinger ved feil</string>
|
||||
<string name="ns_create_announcements_from_carbs_req_title">Opprett varslinger hvis det er nødvendig med karbohydrater</string>
|
||||
<string name="ns_create_announcements_from_errors_summary">Opprett varslinger i Nightscout ved feil eller meldinger (også synlig i Careportal under Behandlinger)</string>
|
||||
<string name="ns_create_announcements_from_errors_summary">Opprett varslinger i Nightscout ved feil eller meldinger (også synlig i Helseportal under Behandlinger)</string>
|
||||
<string name="ns_create_announcements_from_carbs_req_summary">Opprett Nightscout-meldinger ved behov for karbohydrater</string>
|
||||
<string name="description_ns_client">Synkroniserer dine data med Nightscout v1 API</string>
|
||||
<string name="description_ns_client_v3">Synkroniserer dine data med Nightscout v3 API</string>
|
||||
|
@ -54,11 +54,11 @@
|
|||
<string name="ns_receive_temp_target">Motta midlertidige mål</string>
|
||||
<string name="ns_receive_temp_target_summary">Aksepter midlertidige mål angitt med NS eller AAPSClient</string>
|
||||
<string name="ns_receive_profile_switch">Motta profilbytter</string>
|
||||
<string name="ns_receive_profile_switch_summary">Aksepter profilbytter som er angitt via NS eller NSClient</string>
|
||||
<string name="ns_receive_profile_switch_summary">Aksepter profilbytter som er angitt via NS eller AAPSClient</string>
|
||||
<string name="ns_receive_offline_event">Motta APS offline hendelser</string>
|
||||
<string name="ns_receive_offline_event_summary">Aksepter APS offline hendelser lagt inn gjennom NS eller AAPSClient</string>
|
||||
<string name="ns_receive_tbr_eb">Motta TBR og EB</string>
|
||||
<string name="ns_receive_tbr_eb_summary">Godta TBR og EB beregninger fra tilleggsmodul</string>
|
||||
<string name="ns_receive_tbr_eb_summary">Godta midlertidig basal og forlenget bolus lagt inn fra en annen instans</string>
|
||||
<string name="ns_receive_insulin">Motta insulin</string>
|
||||
<string name="ns_receive_insulin_summary">Aksepter insulin angitt via NS eller AAPSClient (enhetene er ikke dosert, kun beregnet mot IOB)</string>
|
||||
<string name="ns_receive_carbs">Motta karbohydrater</string>
|
||||
|
|
|
@ -180,6 +180,78 @@
|
|||
|
||||
<!-- DataBroadcast-->
|
||||
<string name="data_broadcaster">Data Broadcaster</string>
|
||||
<string name="data_broadcaster_description" translatable="false">Broadcast data to other apps like Garmin watch</string>
|
||||
<string name="data_broadcaster_short">DBRO</string>
|
||||
<string name="data_broadcaster_description">Broadcast data to Garmin\'s G-Watch Wear App</string>
|
||||
|
||||
<!-- GarminPlugin -->
|
||||
<string name="garmin">Garmin</string>
|
||||
<string name="garmin_description">Connection to Garmin device (Fenix, Edge, …)</string>
|
||||
<string name="key_garmin_settings">Garmin settings</string>
|
||||
|
||||
<!-- Wear-->
|
||||
<string name="wear_shortname">WEAR</string>
|
||||
<string name="description_wear">Monitor and control AAPS using your WearOS watch.</string>
|
||||
<string name="no_watch_connected">(No Watch Connected)</string>
|
||||
<string name="pump_status">Pump status</string>
|
||||
<string name="loop_status">Loop status</string>
|
||||
<string name="wizard_result">Calc. Wizard:\nInsulin: %1$.2fU\nCarbs: %2$dg</string>
|
||||
<string name="quick_wizard_not_available">Selected quickwizard no longer available, please refresh your tile</string>
|
||||
<string name="quick_wizard_message">QuickWizard: %1$s\nInsulin: %2$.2fU\nCarbs: %3$dg</string>
|
||||
<string name="wear_action_tempt_preset_error">Temptarget unknown preset: %1$s</string>
|
||||
<string name="wear_action_tempt_cancel_message">Cancelling running Temp-Targets?</string>
|
||||
<string name="wear_action_tempt_unit_error">Different units used on watch and phone!</string>
|
||||
<string name="wear_action_tempt_zero_message">Zero-Temp-Target - cancelling running Temp-Targets?</string>
|
||||
<string name="wear_action_tempt_min_bg_error">Min-BG out of range!</string>
|
||||
<string name="wear_action_tempt_max_bg_error">Max-BG out of range!</string>
|
||||
<string name="wear_action_tempt_manual_range_message">Temptarget:\nMin: %1$s\nMax: %2$s\nDuration: %3$s</string>
|
||||
<string name="wear_action_tempt_manual_message">Temptarget:\nTarget: %1$s\nDuration: %2$s</string>
|
||||
<string name="wear_action_tempt_preset_message">Temptarget:\nReason: %1$s\nTarget: %2$s\nDuration: %3$s</string>
|
||||
<string name="no_success">not successful - please check phone</string>
|
||||
<string name="wear_settings">Wear settings</string>
|
||||
<string name="wearcontrol_title">Controls from Watch</string>
|
||||
<string name="wearcontrol_summary">Set Temp-Targets and enter Treatments from the watch.</string>
|
||||
<string name="wear_wizard_settings_summary">Calculations included in the Wizard result:</string>
|
||||
<string name="wear_general_settings">General Settings</string>
|
||||
<string name="wear_notifysmb_title">Notify on SMB</string>
|
||||
<string name="wear_notifysmb_summary">Show SMB on the watch like a standard bolus.</string>
|
||||
<string name="wear_custom_watchface_settings">Custom Watchface Settings</string>
|
||||
<string name="wear_custom_watchface_authorization_title">Custom Watchface Authorization</string>
|
||||
<string name="wear_custom_watchface_authorization_summary">Authorize loaded custom watchface to change and lock some watch display settings to suit watchface design</string>
|
||||
<string name="wear_custom_watchface">Custom Watchface: %1$s</string>
|
||||
<string name="wear_load_watchface">Load Watchface</string>
|
||||
<string name="wear_infos_watchface">Infos Watchface</string>
|
||||
<string name="wear_export_watchface">Export template</string>
|
||||
<string name="wear_new_custom_watchface_exported">Custom watchface template exported</string>
|
||||
<string name="resend_all_data">Resend All Data</string>
|
||||
<string name="open_settings_on_wear">Open Settings on Wear</string>
|
||||
<string name="cwf_infos_pref_locked">List of prefs locked by the Watchface</string>
|
||||
<string name="cwf_infos_pref_required">List of prefs required for the Watchface</string>
|
||||
<string name="cwf_infos_view_title">List of fields included into the Watchface</string>
|
||||
<string name="pump_fetching_data">trying to fetch data from pump.</string>
|
||||
<string name="pump_old_data">TDD: Still old data! Cannot load from pump.</string>
|
||||
<string name="units_short">U</string>
|
||||
<string name="grams_short">g</string>
|
||||
<string name="hour_short">h</string>
|
||||
<string name="no_active_profile">No active profile switch!</string>
|
||||
<string name="profile_message">Profile:\n\nTimeshift: %1$d\nPercentage: %2$d%%\"</string>
|
||||
<string name="target_only_aps_mode">Targets only apply in APS mode!</string>
|
||||
<string name="no_history">No history data!</string>
|
||||
<string name="temp_target">Temp Target</string>
|
||||
<string name="until">until</string>
|
||||
<string name="default_range">DEFAULT RANGE</string>
|
||||
<string name="target">target</string>
|
||||
<string name="rate_duration">Rate: %1$.2fU/h (%2$.2f%%) \nDuration %3$d min</string>
|
||||
<string name="no_profile">No profile loaded</string>
|
||||
<string name="aps_only">Only apply in APS mode!</string>
|
||||
<string name="last_aps_result_na">Last result not available!</string>
|
||||
<string name="loop_status_closed">CLOSED LOOP</string>
|
||||
<string name="loop_status_open">OPEN LOOP</string>
|
||||
<string name="loop_status_disabled">LOOP DISABLED</string>
|
||||
<string name="aps">APS</string>
|
||||
<string name="last_run">Last run</string>
|
||||
<string name="last_enact">Last Enact</string>
|
||||
<string name="tdd_line">%1$.2fU %1$.0f%%</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="weighted">weighted</string>
|
||||
|
||||
</resources>
|
23
plugins/sync/src/main/res/xml/pref_garmin.xml
Normal file
23
plugins/sync/src/main/res/xml/pref_garmin.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="@string/key_garmin_settings"
|
||||
app:initialExpandedChildrenCount="0"
|
||||
android:title="@string/garmin">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="true"
|
||||
android:key="communication_http"
|
||||
android:title="Local HTTP server" />
|
||||
|
||||
<EditTextPreference
|
||||
android:defaultValue="28891"
|
||||
android:digits="0123456789"
|
||||
android:inputType="numberDecimal"
|
||||
android:key="communication_http_port"
|
||||
android:title="Local HTTP server port" />
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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<String, Any>): URI {
|
||||
return URI("http://foo?" + params.entries.joinToString(separator = "&") { (k, v) ->
|
||||
"$k=$v"})
|
||||
}
|
||||
|
||||
private fun createHeartRate(@Suppress("SameParameterValue") heartRate: Int) = mapOf<String, Any>(
|
||||
"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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -36,13 +36,13 @@
|
|||
<string name="combo_invalid_setup">Ugyldig oppsett av pumpen. Les dokumentasjonen og sjekk at Quick Info menyen heter QUICK INFO ved hjelp av 360 programvaren.</string>
|
||||
<string name="combo_actvity_reading_basal_profile">Leser basalprofil</string>
|
||||
<string name="combo_bolus_rejected_due_to_pump_history_change">Pumpe historikken har blitt endret siden bolus kalkuleringen ble utført. Bolus har ikke blitt levert. Vennligst rekalkuler om bolus fortsatt er nødvendig.</string>
|
||||
<string name="combo_error_updating_treatment_record">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.</string>
|
||||
<string name="combo_error_updating_treatment_record">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.</string>
|
||||
<string name="combo_high_temp_rejected_due_to_pump_history_changes">Avviser høy temp target siden kalkuleringen ikke tok hensyn til nylige endringer i pumpe historikken</string>
|
||||
<string name="combo_activity_checking_pump_state">Oppdaterer pumpestatus</string>
|
||||
<string name="combo_warning_pump_basal_rate_changed">Basal dosen i pumpen har blitt endret og vil i løpet av kort tid bli oppdatert</string>
|
||||
<string name="combo_error_failure_reading_changed_basal_rate">Basalsats endret i pumpe, men lesing av den feilet</string>
|
||||
<string name="combo_activity_checking_for_history_changes">Sjekker for endringer i historikken</string>
|
||||
<string name="combo_error_multiple_boluses_with_identical_timestamp">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.</string>
|
||||
<string name="combo_error_multiple_boluses_with_identical_timestamp">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.</string>
|
||||
<string name="combo_check_date">Den siste bolus er eldre enn 24t eller er i fremtiden. Vennligst sjekk at datoen i pumpen er korrekt.</string>
|
||||
<string name="combo_suspious_bolus_time">Tid/dato for levert bolus i pumpen er trolig feil, og IOB beregningen blir da feil. Vennligst sjekk pumpens tid/dato.</string>
|
||||
<string name="combo_bolus_count">Antall boluser</string>
|
||||
|
|
|
@ -100,7 +100,7 @@ knappene samtidig for å avbryte parringen)\n
|
|||
<string name="combov2_letting_emulated_100_tbr_finish">Lar aktive emulert 100% TBR få avslutte</string>
|
||||
<string name="combov2_ignoring_redundant_100_tbr">Ignorerer redundant 100% TBR forespørsel</string>
|
||||
<string name="combov2_hit_unexpected_tbr_limit">Uventet begrensning oppsto ved justering av TBR: målprosenten var %1$d%%, nådde grense på %2$d%%</string>
|
||||
<string name="combov2_cannot_set_absolute_tbr_if_basal_zero">Kan ikke sette absolutt TBR hvis basalraten er null</string>
|
||||
<string name="combov2_cannot_set_absolute_tbr_if_basal_zero">Kan ikke sette absolutt TBR hvis basaldosen er null</string>
|
||||
<string name="combov2_pair_with_pump_summary">Sammenkoble AndroidAPS og Android med en ikke-tilkoblet Accu-Chek Combo pumpe</string>
|
||||
<string name="combov2_unpair_pump_summary">Koble fra AndroidAPS og Android fra den ilkoblede Accu-Chek Combo pumpen</string>
|
||||
<string name="combov2_unknown_tbr_detected">Ukjent TBR ble oppdaget og stoppet; prosent: %1$d%%, gjenværende varighet: %2$s</string>
|
||||
|
|
|
@ -107,9 +107,9 @@
|
|||
<string name="bolusspeed">Bolus hastighet</string>
|
||||
<string name="selectedpump">Valgt pumpe</string>
|
||||
<string name="rs_loginsulinchange_title">Logg reservoar bytte</string>
|
||||
<string name="rs_loginsulinchange_summary">Legg til \"Insulinbytte\" i Careportal når den oppdages i historikken</string>
|
||||
<string name="rs_loginsulinchange_summary">Legg til \"Insulinbytte\" i Helseportal når den oppdages i historikken</string>
|
||||
<string name="rs_logcanulachange_title">Logg kanyle bytte</string>
|
||||
<string name="rs_logcanulachange_summary">Legg til \"Kanylebytte\" i Careportal når den oppdages i historikken</string>
|
||||
<string name="rs_logcanulachange_summary">Legg til \"Kanylebytte\" i Helseportal når den oppdages i historikken</string>
|
||||
<string name="pin1">PIN1</string>
|
||||
<string name="pin2">PIN2</string>
|
||||
<string name="press_ok_on_the_pump">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.</string>
|
||||
|
|
|
@ -85,10 +85,10 @@
|
|||
<string name="apsIncarnationNo">aps_incarnation_no</string>
|
||||
<string name="pumpserialno">pump_serial_no</string>
|
||||
<string name="diaconn_g8_loginsulinchange_title">Logg reservoar bytte</string>
|
||||
<string name="diaconn_g8_loginsulinchange_summary">Legg til \"Insulinbytte\" i Careportal når den oppdages i historikken</string>
|
||||
<string name="diaconn_g8_loginsulinchange_summary">Legg til \"Insulinbytte\" i Helseportal når den oppdages i historikken</string>
|
||||
<string name="diaconn_g8_logcanulachange_title">Logg bytte av kanyle</string>
|
||||
<string name="diaconn_g8_logcanulachange_summary">Legg til \"Bytte av injeksjonssted\" i Careportal når den oppdages i historikken</string>
|
||||
<string name="diaconn_g8_logbatterychange_summary">Legg til \"Bytte av batteri\" i Careportal når den oppdages i historikken</string>
|
||||
<string name="diaconn_g8_logcanulachange_summary">Legg til \"Bytte av injeksjonssted\" i Helseportal når den oppdages i historikken</string>
|
||||
<string name="diaconn_g8_logbatterychange_summary">Legg til \"Bytte av batteri\" i Helseportal når den oppdages i historikken</string>
|
||||
<string name="diaconn_g8_logbatterychange_title">Logg batteri bytte</string>
|
||||
<string name="diaconn_g8_logsyncinprogress">Logg synkronisering pågår</string>
|
||||
<string name="diaconn_g8_loginsulinshorage">Lavt insulinnivå</string>
|
||||
|
@ -145,7 +145,7 @@
|
|||
<string name="diaconn_g8_errorcode_15">Når basal oppsett er fullført, kan basaldoseringer startes.</string>
|
||||
<string name="diaconn_g8_errotpreceivedyet">Kommandoen ble ikke utført. Vennligst prøv igjen.</string>
|
||||
<string name="diaconn_g8_logtubechange_title">Logg bytte av slangesett</string>
|
||||
<string name="diaconn_g8_logtubechange_summary">Legg til \"Slangesettbytte\" i Careportal når den oppdages i historikken</string>
|
||||
<string name="diaconn_g8_logtubechange_summary">Legg til \"Slangesettbytte\" i Helseportal når den oppdages i historikken</string>
|
||||
<string name="diaconn_g8_logtempstart">Temp Basal startet</string>
|
||||
<string name="diaconn_g8_errorcode_32">Vel Lav Glukose Stopp (LGS) er injeksjoner begrenset</string>
|
||||
<string name="diaconn_g8_errorcode_33">LGS status er PÅ. PÅ kommando er nektet.</string>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<string name="reservoir_level"> %.2f E</string>
|
||||
<string name="battery_voltage"> %.2f V</string>
|
||||
<string name="basal_type_label">Basal type</string>
|
||||
<string name="basal_rate_label">Basalrate</string>
|
||||
<string name="basal_rate_label">Basaldose</string>
|
||||
<string name="current_basal_rate"> %.2f E/t</string>
|
||||
<string name="pump_type_label">Pumpetype</string>
|
||||
<string name="fw_version_label">FW versjon</string>
|
||||
|
@ -85,7 +85,7 @@
|
|||
<string name="press_next">Trykk <b>Neste</b> for å fortsette.</string>
|
||||
<string name="press_next_to_start_activation">Trykk <b>Neste</b> for å starte aktivering.</string>
|
||||
<string name="remove_safety_lock">Fjern sikkerhetslåsen. Koble pumpen til kroppen. Trykk nål-knappen.</string>
|
||||
<string name="activating_pump">Aktiverer pumpe og setter basalrate. Vennligst vent.</string>
|
||||
<string name="activating_pump">Aktiverer pumpe og setter basaldose. Vennligst vent.</string>
|
||||
<string name="activating_error">Kunne ikke aktivere, trykk <b>Prøv igjen</b> for å prøve igjen.</string>
|
||||
<string name="activating_complete">Ny patch aktivert. %.2f enheter gjenstår.</string>
|
||||
<string name="press_OK_to_return">Trykk <b>OK</b> for å gå tilbake til hovedskjermen.</string>
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
<string name="alarm_battery_out">Batterij leeg</string>
|
||||
<string name="alarm_no_calibration">Geen kalibratie</string>
|
||||
<string name="pump_time_update_failed">Update van pomp tijdzone mislukt, snooze bericht en vernieuw handmatig.</string>
|
||||
<string name="bolus_error">Bolus fout</string>
|
||||
<!-- wizard-->
|
||||
<string name="retry">Opnieuw</string>
|
||||
<string name="next">Volgende</string>
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
<string name="alarm_battery_out">Baterie descărcată</string>
|
||||
<string name="alarm_no_calibration">Fără calibrare</string>
|
||||
<string name="pump_time_update_failed">Actualizarea fusului orar al pompei a eșuat, amână mesajul și actualizează manual.</string>
|
||||
<string name="bolus_error">Eroare bolus</string>
|
||||
<!-- wizard-->
|
||||
<string name="retry">Încearcă din nou</string>
|
||||
<string name="next">Înainte</string>
|
||||
|
@ -101,6 +102,8 @@
|
|||
<string name="press_retry_or_discard">Apasă <b>Înainte</b> pentru a relua activarea sau <b>Renunțare</b> pentru a reseta statusul activării.</string>
|
||||
<string name="reading_activation_status">Te rog, așteaptă. Se citește starea de activare din pompă.</string>
|
||||
<!-- settings-->
|
||||
<string name="enable_pump_unreachable_alert_summary">Alertă de inaccesibilitate activată forțat, deoarece patchul Medtrum poate eșua și să nu fie accesibil.</string>
|
||||
<string name="pump_unreachable_threshold_minutes_summary">Recomandat să se seteze la 30 de minute, deoarece patchul Medtrum poate eșua și să nu fie accesibil.</string>
|
||||
<string name="sn_input_title">Număr de serie</string>
|
||||
<string name="sn_input_summary">Introdu numărul de serie al bazei pompei.</string>
|
||||
<string name="sn_input_invalid">Număr de serie invalid!</string>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<string name="medtrum_pump_description">Интеграция с помпами Medtrum Nano и Medtrum 300U</string>
|
||||
<string name="medtrum_pump_setting">Настройки Medtrum</string>
|
||||
<string name="pump_error">Ошибка помпы: %1$s !! </string>
|
||||
<string name="pump_warning">Помпа - Предупреждение: %1$s </string>
|
||||
<string name="pump_is_suspended">Помпа приостановлена</string>
|
||||
<string name="pump_is_suspended_hour_max">Помпа приостановлена из-за превышения максимального количества инсулина в час</string>
|
||||
<string name="pump_is_suspended_day_max">Помпа приостановлена в связи с превышением максимального допустимого количества инсулина в сутки</string>
|
||||
|
@ -52,6 +53,7 @@
|
|||
<string name="alarm_battery_out">Батарея разряжена</string>
|
||||
<string name="alarm_no_calibration">Нет калибровки</string>
|
||||
<string name="pump_time_update_failed">Не удалось обновить часовой пояс помпы, закройте сообщение и обновите вручную.</string>
|
||||
<string name="bolus_error">Болюс - ошибка</string>
|
||||
<!-- wizard-->
|
||||
<string name="retry">Повторить</string>
|
||||
<string name="next">Далее</string>
|
||||
|
@ -100,12 +102,16 @@
|
|||
<string name="press_retry_or_discard">Нажмите <b>Далее</b>, чтобы возобновить активацию или <b>Discard</b> для сброса статуса активации.</string>
|
||||
<string name="reading_activation_status">Пожалуйста, подождите, получение статуса активации с помпы.</string>
|
||||
<!-- settings-->
|
||||
<string name="enable_pump_unreachable_alert_summary">Принудительно включена сигнализация о недоступности помпы, так как патч Medtrum может выйти из строя и быть недоступным.</string>
|
||||
<string name="pump_unreachable_threshold_minutes_summary">Рекомендуется установить значение в 30 минут, так как патч Medtrum может выйти из строя и быть недоступным.</string>
|
||||
<string name="sn_input_title">Серийный номер</string>
|
||||
<string name="sn_input_summary">Введите серийный номер основания вашей помпы.</string>
|
||||
<string name="sn_input_invalid">Неверный серийный номер!</string>
|
||||
<string name="pump_unsupported">Помпа не тестировалась: %1$d! Свяжитесь с нами в discord или github</string>
|
||||
<string name="alarm_setting_title">Настройки оповещений</string>
|
||||
<string name="alarm_setting_summary">Выберите предпочитаемые параметры оповещений помпы.</string>
|
||||
<string name="pump_warning_notification_title">Уведомление об оповещениях помпы</string>
|
||||
<string name="pump_warning_notification_summary">Показывать уведомления о некритических оповещениях помпы: низком заряде батареи, низком запасе инсулина (20 единиц) и приближающемся истечении срока работы. Рекомендуется оставить включенным, когда звук оповещений выключен.</string>
|
||||
<string name="patch_expiration_title">Окончание срока действия патча</string>
|
||||
<string name="patch_expiration_summary">Если включено, то патч закончит свое действие через 3 дня, с дополнительными 8 часами после этого.</string>
|
||||
<string name="hourly_max_insulin_title">Максимальное количество инсулина в час</string>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<string name="profile_set_ok">Profilen ble angitt</string>
|
||||
<string name="suspend_delivery_is_unconfirmed">Pausing av insulintilførsel ble ikke bekreftet! Vennligst oppdater Pod-status fra Omnipod-fanen og gjenoppta insulinlevering om nødvendig.</string>
|
||||
<string name="insulin_delivery_suspended">Insulintilførsel er pauset</string>
|
||||
<string name="timezone_on_pod_is_different_from_the_timezone">Tidssone på pod er forskjellig fra tidssonen på telefon. Basalrater er feil. Bytt profil for å korrigere</string>
|
||||
<string name="timezone_on_pod_is_different_from_the_timezone">Tidssone på pod er forskjellig fra tidssonen på telefon. Basaldoser er feil. Bytt profil for å korrigere</string>
|
||||
<string name="failed_to_set_the_new_basal_profile">Kunne ikke sette ny basalprofil. Insulintilførsel er pauset</string>
|
||||
<string name="setting_basal_profile_might_have_failed">Endring av basalprofil kan ha feilet. Insulinlevering kan bli stoppet! Vennligst velg Oppdater Pod fra Omnipod-fanen og velg gjenoppta levering hvis nødvendig.</string>
|
||||
<string name="bolus_delivery_status_uncertain">Status for levering av bolusdoser er usikker. Oppdater pod-statusen for å verifisere.</string>
|
||||
|
|
|
@ -38,6 +38,43 @@
|
|||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:background="?attr/dialogTitleBackground"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:text="2.35U 28g"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/percent_used"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:text="50%"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textColor="?attr/bolusColor"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/spacer"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -175,43 +212,6 @@
|
|||
|
||||
</TableLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:background="?attr/dialogTitleBackground"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:text="2.35U 28g"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/percent_used"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:text="50%"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textColor="?attr/bolusColor"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/notes_layout"
|
||||
layout="@layout/notes" />
|
||||
|
|
|
@ -124,7 +124,7 @@
|
|||
<string name="dpv_default_profile">DPV standardprofil</string>
|
||||
<string name="invalid_pct">Ugyldig % verdi</string>
|
||||
<!-- Other dialogs -->
|
||||
<string name="basal_rate">Basalrate</string>
|
||||
<string name="basal_rate">Basaldose</string>
|
||||
<!-- BolusProgressDialog-->
|
||||
<string name="stop_pressed">STOPP er trykket</string>
|
||||
<!--QuickWizard-->
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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<Int, Drawable?>()
|
||||
private val dynColor = mutableMapOf<Int, Int>()
|
||||
private val dynFontColor = mutableMapOf<Int, Int>()
|
||||
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)
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
<string name="pref_show_avgdelta">Vis Gj.snittDelta</string>
|
||||
<string name="pref_show_phone_battery">Vis telefonbatteri</string>
|
||||
<string name="pref_show_rig_battery">Vis riggens batteri</string>
|
||||
<string name="pref_show_basal_rate">Vis basalrate</string>
|
||||
<string name="pref_show_basal_rate">Vis basaldose</string>
|
||||
<string name="pref_show_loop_status">Vis loop status</string>
|
||||
<string name="pref_show_bg">Vis BS</string>
|
||||
<string name="pref_show_bgi">Vis BGI</string>
|
||||
|
@ -109,7 +109,7 @@
|
|||
<string name="action_ecarbs">eKarbo</string>
|
||||
<string name="action_percentage">Prosent</string>
|
||||
<string name="action_start_min">Start [min]</string>
|
||||
<string name="action_duration_h">Varighet [h]</string>
|
||||
<string name="action_duration_h">Varighet [t]</string>
|
||||
<string name="action_insulin">Insulin</string>
|
||||
<string name="action_preset_1">Forhåndsinnstilling 1</string>
|
||||
<string name="action_preset_2">Forhåndsinnstilling 2</string>
|
||||
|
|
Loading…
Reference in a new issue