Merge branch 'dev' into kts

This commit is contained in:
Milos Kozak 2023-10-14 22:19:45 +02:00
commit 5edc363733
71 changed files with 5405 additions and 163 deletions

View file

@ -119,6 +119,7 @@ android {
flavorDimensions.add("standard")
productFlavors {
create("full") {
isDefault = true
applicationId = "info.nightscout.androidaps"
dimension = "standard"
resValue("string", "app_name", "AAPS")

View file

@ -43,6 +43,7 @@ import app.aaps.plugins.configuration.maintenance.MaintenancePlugin
import app.aaps.plugins.constraints.safety.SafetyPlugin
import app.aaps.plugins.insulin.InsulinOrefFreePeakPlugin
import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin
import app.aaps.plugins.sync.garmin.GarminPlugin
import app.aaps.plugins.main.general.wear.WearPlugin
import app.aaps.plugins.sensitivity.SensitivityAAPSPlugin
import app.aaps.plugins.sensitivity.SensitivityOref1Plugin
@ -128,6 +129,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang
@Inject lateinit var nsSettingStatus: NSSettingsStatus
@Inject lateinit var openHumansUploaderPlugin: OpenHumansUploaderPlugin
@Inject lateinit var diaconnG8Plugin: DiaconnG8Plugin
@Inject lateinit var garminPlugin: GarminPlugin
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
@ -229,6 +231,7 @@ class MyPreferenceFragment : PreferenceFragmentCompat(), OnSharedPreferenceChang
addPreferencesFromResource(app.aaps.plugins.configuration.R.xml.pref_datachoices, rootKey)
addPreferencesFromResourceIfEnabled(maintenancePlugin, rootKey)
addPreferencesFromResourceIfEnabled(openHumansUploaderPlugin, rootKey)
addPreferencesFromResourceIfEnabled(garminPlugin, rootKey)
}
initSummary(preferenceScreen, pluginId != -1)
preprocessPreferences()

View file

@ -22,6 +22,7 @@ import app.aaps.plugins.insulin.InsulinOrefRapidActingPlugin
import app.aaps.plugins.insulin.InsulinOrefUltraRapidActingPlugin
import app.aaps.plugins.main.general.actions.ActionsPlugin
import app.aaps.plugins.main.general.food.FoodPlugin
import app.aaps.plugins.sync.garmin.GarminPlugin
import app.aaps.plugins.main.general.overview.OverviewPlugin
import app.aaps.plugins.main.general.persistentNotification.PersistentNotificationPlugin
import app.aaps.plugins.main.general.smsCommunicator.SmsCommunicatorPlugin
@ -465,6 +466,12 @@ abstract class PluginsListModule {
@IntKey(610)
abstract fun bindAvgSmoothingPlugin(plugin: AvgSmoothingPlugin): PluginBase
@Binds
@AllConfigs
@IntoMap
@IntKey(623)
abstract fun bindGarminPlugin(plugin: GarminPlugin): PluginBase
@Qualifier
annotation class AllConfigs

View file

@ -14,6 +14,7 @@ import app.aaps.activities.PreferencesActivity
import app.aaps.core.interfaces.notifications.Notification
import app.aaps.core.interfaces.nsclient.NSAlarm
import app.aaps.core.interfaces.rx.bus.RxBus
import app.aaps.core.interfaces.rx.events.EventDismissNotification
import app.aaps.core.interfaces.ui.UiInteraction
import app.aaps.core.main.events.EventNewNotification
import app.aaps.core.ui.toast.ToastUtils
@ -169,6 +170,10 @@ class UiInteractionImpl @Inject constructor(
}
}
override fun dismissNotification(id: Int) {
rxBus.send(EventDismissNotification(id))
}
override fun addNotification(id: Int, text: String, level: Int) {
rxBus.send(EventNewNotification(Notification(id, text, level)))
}

View file

@ -45,6 +45,7 @@ android {
flavorDimensions.add("standard")
productFlavors {
create("full") {
isDefault = true
dimension = "standard"
}
create("pumpcontrol") {

View file

@ -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();

View file

@ -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"),

View file

@ -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
}

View file

@ -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()

View file

@ -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)
}
}
}

View file

@ -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()

View file

@ -68,6 +68,7 @@ interface UiInteraction {
fun runCareDialog(fragmentManager: FragmentManager, options: EventType, @StringRes event: Int)
fun dismissNotification(id: Int)
fun addNotification(id: Int, text: String, level: Int)
fun addNotificationValidFor(id: Int, text: String, level: Int, validMinutes: Int)
fun addNotificationWithSound(id: Int, text: String, level: Int, @RawRes soundId: Int?)

View file

@ -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>

View file

@ -3,6 +3,7 @@ android {
flavorDimensions = ["standard"]
productFlavors {
full {
getIsDefault().set(true)
dimension "standard"
}
pumpcontrol {

View file

@ -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'
}

View file

@ -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>

View file

@ -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>

View file

@ -187,7 +187,9 @@ data class UserEntry(
Overview, //From OverViewPlugin
Stats, //From Stat Activity
Aaps, // MainApp
GarminDevice,
Unknown //if necessary
,
;
companion object {

File diff suppressed because it is too large Load diff

View file

@ -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())
}
}

View file

@ -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>)
}

View file

@ -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
}

View file

@ -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>

View file

@ -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>

View file

@ -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) {

View file

@ -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)

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -22,7 +22,7 @@ import dagger.android.ContributesAndroidInjector
SkinsUiModule::class,
ActionsModule::class,
WearModule::class,
OverviewModule::class
OverviewModule::class,
]
)

View file

@ -11,7 +11,6 @@ import app.aaps.core.interfaces.rx.bus.RxBus
import app.aaps.core.interfaces.rx.events.EventAutosensCalculationFinished
import app.aaps.core.interfaces.rx.events.EventDismissBolusProgressIfRunning
import app.aaps.core.interfaces.rx.events.EventLoopUpdateGui
import app.aaps.core.interfaces.rx.events.EventMobileDataToWear
import app.aaps.core.interfaces.rx.events.EventMobileToWear
import app.aaps.core.interfaces.rx.events.EventOverviewBolusProgress
import app.aaps.core.interfaces.rx.events.EventPreferenceChange
@ -112,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)))
}
}
}

View file

@ -31,6 +31,7 @@ import app.aaps.core.interfaces.rx.AapsSchedulers
import app.aaps.core.interfaces.rx.bus.RxBus
import app.aaps.core.interfaces.rx.events.EventMobileToWear
import app.aaps.core.interfaces.rx.events.EventWearUpdateGui
import app.aaps.core.interfaces.rx.weardata.CwfMetadataKey
import app.aaps.core.interfaces.rx.weardata.EventData
import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.core.interfaces.ui.UiInteraction
@ -70,6 +71,7 @@ import app.aaps.plugins.main.R
import dagger.android.HasAndroidInjector
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.json.JSONObject
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
@ -1266,10 +1268,19 @@ class DataHandlerMobile @Inject constructor(
private fun handleGetCustomWatchface(command: EventData.ActionGetCustomWatchface) {
val customWatchface = command.customWatchface
aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.customWatchfaceData.json}")
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)
}
}

View file

@ -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) {

View file

@ -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>

View file

@ -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>

View file

@ -7,6 +7,8 @@ import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData
import app.aaps.core.interfaces.nsclient.StoreDataForDb
import app.aaps.core.interfaces.sync.DataSyncSelectorXdrip
import app.aaps.core.interfaces.sync.XDripBroadcast
import app.aaps.plugins.sync.garmin.LoopHub
import app.aaps.plugins.sync.garmin.LoopHubImpl
import app.aaps.plugins.sync.nsShared.NSClientFragment
import app.aaps.plugins.sync.nsShared.StoreDataForDbImpl
import app.aaps.plugins.sync.nsclient.data.NSSettingsStatusImpl
@ -82,6 +84,7 @@ abstract class SyncModule {
@Binds fun bindDataSyncSelectorXdripInterface(dataSyncSelectorXdripImpl: DataSyncSelectorXdripImpl): DataSyncSelectorXdrip
@Binds fun bindStoreDataForDb(storeDataForDbImpl: StoreDataForDbImpl): StoreDataForDb
@Binds fun bindXDripBroadcastInterface(xDripBroadcastImpl: XdripPlugin): XDripBroadcast
@Binds fun bindLoopHub(loopHub: LoopHubImpl): LoopHub
}
}

View file

@ -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)
}
}
}

View file

@ -0,0 +1,245 @@
package app.aaps.plugins.sync.garmin
import androidx.annotation.VisibleForTesting
import app.aaps.core.interfaces.db.GlucoseUnit
import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag
import app.aaps.core.interfaces.plugin.PluginBase
import app.aaps.core.interfaces.plugin.PluginDescription
import app.aaps.core.interfaces.plugin.PluginType
import app.aaps.core.interfaces.resources.ResourceHelper
import app.aaps.core.interfaces.rx.bus.RxBus
import app.aaps.core.interfaces.rx.events.EventNewBG
import app.aaps.core.interfaces.rx.events.EventPreferenceChange
import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.database.entities.GlucoseValue
import app.aaps.plugins.sync.R
import com.google.gson.JsonObject
import dagger.android.HasAndroidInjector
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import java.net.SocketAddress
import java.net.URI
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.util.*
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.ReentrantLock
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.concurrent.withLock
import kotlin.math.roundToInt
/** Support communication with Garmin devices.
*
* This plugin supports sending glucose values to Garmin devices and receiving
* carbs, heart rate and pump disconnect events from the device. It communicates
* via HTTP on localhost or Garmin's native CIQ library.
*/
@Singleton
class GarminPlugin @Inject constructor(
injector: HasAndroidInjector,
aapsLogger: AAPSLogger,
resourceHelper: ResourceHelper,
private val loopHub: LoopHub,
private val rxBus: RxBus,
private val sp: SP,
) : PluginBase(
PluginDescription()
.mainType(PluginType.SYNC)
.pluginName(R.string.garmin)
.shortName(R.string.garmin)
.description(R.string.garmin_description)
.preferencesId(R.xml.pref_garmin),
aapsLogger, resourceHelper, injector
) {
/** HTTP Server for local HTTP server communication (device app requests values) .*/
private var server: HttpServer? = null
private val disposable = CompositeDisposable()
@VisibleForTesting
var clock: Clock = Clock.systemUTC()
private val valueLock = ReentrantLock()
@VisibleForTesting
var newValue: Condition = valueLock.newCondition()
private var lastGlucoseValueTimestamp: Long? = null
private val glucoseUnitStr get() = if (loopHub.glucoseUnit == GlucoseUnit.MGDL) "mgdl" else "mmoll"
private fun onPreferenceChange(event: EventPreferenceChange) {
aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}")
setupHttpServer()
}
override fun onStart() {
super.onStart()
aapsLogger.info(LTag.GARMIN, "start")
disposable.add(
rxBus
.toObservable(EventPreferenceChange::class.java)
.observeOn(Schedulers.io())
.subscribe(::onPreferenceChange)
)
setupHttpServer()
}
private fun setupHttpServer() {
if (sp.getBoolean("communication_http", false)) {
val port = sp.getInt("communication_http_port", 28891)
if (server != null && server?.port == port) return
aapsLogger.info(LTag.GARMIN, "starting HTTP server on $port")
server?.close()
server = HttpServer(aapsLogger, port).apply {
registerEndpoint("/get", ::onGetBloodGlucose)
}
} else if (server != null) {
aapsLogger.info(LTag.GARMIN, "stopping HTTP server")
server?.close()
server = null
}
}
override fun onStop() {
disposable.clear()
aapsLogger.info(LTag.GARMIN, "Stop")
server?.close()
server = null
super.onStop()
}
/** Receive new blood glucose events.
*
* Stores new blood glucose values in lastGlucoseValue to make sure we return
* these values immediately when values are requested by Garmin device.
* Sends a message to the Garmin devices via the ciqMessenger. */
@VisibleForTesting
fun onNewBloodGlucose(event: EventNewBG) {
val timestamp = event.glucoseValueTimestamp ?: return
aapsLogger.info(LTag.GARMIN, "onNewBloodGlucose ${Date(timestamp)}")
valueLock.withLock {
if ((lastGlucoseValueTimestamp?: 0) >= timestamp) return
lastGlucoseValueTimestamp = timestamp
newValue.signalAll()
}
}
/** Gets the last 2+ hours of glucose values. */
@VisibleForTesting
fun getGlucoseValues(): List<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")
}
}
}

View file

@ -0,0 +1,257 @@
package app.aaps.plugins.sync.garmin
import android.os.StrictMode
import androidx.annotation.VisibleForTesting
import app.aaps.core.interfaces.logging.AAPSLogger
import app.aaps.core.interfaces.logging.LTag
import java.io.*
import java.lang.Thread.UncaughtExceptionHandler
import java.net.*
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.locks.ReentrantLock
import java.util.regex.Pattern
import kotlin.concurrent.withLock
/** Basic HTTP server to communicate with Garmin device via localhost. */
class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val port: Int): Closeable {
private val serverThread: Thread
private val workerExecutor: Executor = Executors.newCachedThreadPool()
private val endpoints: 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) {
var contentLength = Int.MAX_VALUE
if (headers.containsKey("Content-Length")) {
contentLength = headers["Content-Length"]!!.toInt()
}
val keepAlive = ("Keep-Alive" == headers["Connection"])
val contentType = headers["Content-Type"]
if (keepAlive && contentLength == Int.MAX_VALUE) {
throw IOException("keep-alive without content-length for $uri")
}
body = readBody(input, contentLength)
if (("application/x-www-form-urlencoded" == contentType)) {
uri = URI(uri.scheme, uri.userInfo, uri.host, uri.port, uri.path, body, null)
// uri.encodedQuery(body)
body = null
} else if ("application/json" != contentType && body.isNotBlank()) {
body = null
}
} else {
body = null
}
return Pair(uri, body?.takeUnless(String::isBlank))
}
private fun appendHeader(name: String, value: String, header: StringBuilder) {
header.append(name)
header.append(": ")
header.append(value)
header.append("\r\n")
}
}
}

View file

@ -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?
)
}

View file

@ -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()
}
}

View file

@ -2,13 +2,18 @@ package app.aaps.plugins.sync.nsclient.data
import app.aaps.annotations.OpenForTesting
import app.aaps.core.interfaces.configuration.Config
import app.aaps.core.interfaces.notifications.Notification
import app.aaps.core.interfaces.nsclient.ProcessedDeviceStatusData
import app.aaps.core.interfaces.resources.ResourceHelper
import app.aaps.core.interfaces.sharedPreferences.SP
import app.aaps.core.interfaces.ui.UiInteraction
import app.aaps.core.interfaces.utils.DateUtil
import app.aaps.core.interfaces.utils.T
import app.aaps.core.nssdk.interfaces.RunningConfiguration
import app.aaps.core.nssdk.localmodel.devicestatus.NSDeviceStatus
import app.aaps.core.utils.HtmlHelper
import app.aaps.core.utils.JsonHelper
import app.aaps.plugins.sync.R
import javax.inject.Inject
import javax.inject.Singleton
@ -72,24 +77,31 @@ class NSDeviceStatusHandler @Inject constructor(
private val config: Config,
private val dateUtil: DateUtil,
private val runningConfiguration: RunningConfiguration,
private val processedDeviceStatusData: ProcessedDeviceStatusData
private val processedDeviceStatusData: ProcessedDeviceStatusData,
private val uiInteraction: UiInteraction,
private val rh: ResourceHelper
) {
fun handleNewData(deviceStatuses: Array<NSDeviceStatus>) {
var configurationDetected = false
for (i in deviceStatuses.size - 1 downTo 0) {
val nsDeviceStatus = deviceStatuses[i]
updatePumpData(nsDeviceStatus)
updateDeviceData(nsDeviceStatus)
updateOpenApsData(nsDeviceStatus)
updateUploaderData(nsDeviceStatus)
nsDeviceStatus.pump?.let { sp.putBoolean(app.aaps.core.utils.R.string.key_objectives_pump_status_is_available_in_ns, true) } // Objective 0
if (config.NSCLIENT) {
updatePumpData(nsDeviceStatus)
updateDeviceData(nsDeviceStatus)
updateOpenApsData(nsDeviceStatus)
updateUploaderData(nsDeviceStatus)
}
if (config.NSCLIENT && !configurationDetected)
nsDeviceStatus.configuration?.let {
// copy configuration of Insulin and Sensitivity from main AAPS
runningConfiguration.apply(it)
configurationDetected = true // pick only newest
}
if (config.APS) {
nsDeviceStatus.pump?.let { sp.putBoolean(app.aaps.core.utils.R.string.key_objectives_pump_status_is_available_in_ns, true) } // Objective 0
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -182,4 +182,8 @@
<string name="data_broadcaster">Data Broadcaster</string>
<string name="data_broadcaster_description" translatable="false">Broadcast data to other apps like Garmin watch</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>
</resources>

View 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>

View file

@ -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())
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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))
}
}

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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()

View file

@ -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>

View file

@ -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" />

View file

@ -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-->

View file

@ -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)))
}
}

View file

@ -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")

View file

@ -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")

View file

@ -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)

View file

@ -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>