From 4922bc540d5d828bd35d84c27717a7b7079fc3d8 Mon Sep 17 00:00:00 2001 From: olorinmaia Date: Fri, 10 Nov 2023 21:22:48 +0100 Subject: [PATCH 01/70] Add missing dash notification string when there is no active pod --- .../plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 39cd00ef9a..19387cc705 100644 --- a/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -183,7 +183,7 @@ class OmnipodDashPumpPlugin @Inject constructor( if (!podStateManager.isPodRunning) { uiInteraction.addNotification( Notification.OMNIPOD_POD_NOT_ATTACHED, - "Pod not activated", + rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_no_active_pod), Notification.NORMAL ) } else { From fa78a8214e5a774132f3c289bda07945c40e263b Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Mon, 13 Nov 2023 17:05:31 +0100 Subject: [PATCH 02/70] 3.2.0.2-dev --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 80f8350075..dec215bada 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -2,7 +2,7 @@ import org.gradle.api.JavaVersion object Versions { - const val appVersion = "3.2.0.2" + const val appVersion = "3.2.0.2-dev" const val versionCode = 1500 const val ndkVersion = "21.1.6352462" From 07cc0e19c750a14bcbf9cf958ce5ab7b0507e4b8 Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Mon, 13 Nov 2023 22:39:57 +0100 Subject: [PATCH 03/70] Support automatic exchange of communication key for HTTP via connectiq native comm (usually unreliable but could enough for that). --- plugins/sync/build.gradle.kts | 3 + .../connectiq/IConnectIQService.aidl | 30 ++ .../com/garmin/android/connectiq/IQApp.aidl | 12 + .../garmin/android/connectiq/IQDevice.aidl | 11 + .../garmin/android/connectiq/IQMessage.aidl | 20 ++ .../plugins/sync/garmin/GarminApplication.kt | 39 +++ .../aaps/plugins/sync/garmin/GarminClient.kt | 14 + .../aaps/plugins/sync/garmin/GarminDevice.kt | 54 ++++ .../plugins/sync/garmin/GarminDeviceClient.kt | 286 ++++++++++++++++++ .../plugins/sync/garmin/GarminMessenger.kt | 159 ++++++++++ .../aaps/plugins/sync/garmin/GarminPlugin.kt | 130 ++++++-- .../plugins/sync/garmin/GarminReceiver.kt | 36 +++ .../plugins/sync/garmin/GarminSerializer.kt | 254 ++++++++++++++++ .../sync/garmin/GarminSimulatorClient.kt | 186 ++++++++++++ .../aaps/plugins/sync/garmin/HttpServer.kt | 8 +- .../sync/garmin/GarminMessengerTest.kt | 142 +++++++++ .../plugins/sync/garmin/GarminPluginTest.kt | 164 +++++++++- .../sync/garmin/GarminSerializerTest.kt | 92 ++++++ .../sync/garmin/GarminSimulatorClientTest.kt | 86 ++++++ .../plugins/sync/garmin/HttpServerTest.kt | 2 +- 20 files changed, 1694 insertions(+), 34 deletions(-) create mode 100644 plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl create mode 100644 plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl create mode 100644 plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl create mode 100644 plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSerializer.kt create mode 100644 plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt create mode 100644 plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt create mode 100644 plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSerializerTest.kt create mode 100644 plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt diff --git a/plugins/sync/build.gradle.kts b/plugins/sync/build.gradle.kts index b6b885370c..1bea79a6c3 100644 --- a/plugins/sync/build.gradle.kts +++ b/plugins/sync/build.gradle.kts @@ -9,6 +9,9 @@ plugins { android { namespace = "app.aaps.plugins.sync" + buildFeatures { + aidl = true + } } dependencies { diff --git a/plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl b/plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl new file mode 100644 index 0000000000..8aaf0eb761 --- /dev/null +++ b/plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2014 Garmin International Ltd. + * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. + */ +//IConnectIQService +package com.garmin.android.apps.connectmobile.connectiq; + +import com.garmin.android.connectiq.IQDevice; +import com.garmin.android.connectiq.IQApp; +import com.garmin.android.connectiq.IQMessage; + +interface IConnectIQService { + boolean openStore(String applicationID); + List getConnectedDevices(); + List getKnownDevices(); + + // Remote device methods + int getStatus(in IQDevice device); + + // Messages and Commands + oneway void getApplicationInfo(String notificationPackage, String notificationAction, in IQDevice device, String applicationID); + oneway void openApplication(String notificationPackage, String notificationAction, in IQDevice device, in IQApp app); + + // Pending intent will be fired to let the sdk know a message has been transferred. + oneway void sendMessage(in IQMessage message, in IQDevice device, in IQApp app); + oneway void sendImage(in IQMessage image, in IQDevice device, in IQApp app); + + // registers a companion app with the remote service so that it can receive messages from remote device. + oneway void registerApp(in IQApp app, String notificationAction, String notificationPackage); +} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl new file mode 100644 index 0000000000..2115dafd35 --- /dev/null +++ b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl @@ -0,0 +1,12 @@ +/** + * Copyright (C) 2014 Garmin International Ltd. + * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. + */ +package com.garmin.android.connectiq; + +parcelable IQApp { + String applicationID; + int status; + String displayName; + int version; +} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl new file mode 100644 index 0000000000..ec1d4f3217 --- /dev/null +++ b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl @@ -0,0 +1,11 @@ +/** + * Copyright (C) 2014 Garmin International Ltd. + * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. + */ +package com.garmin.android.connectiq; + +parcelable IQDevice { + long deviceIdentifier; + String friendlyName; + int status; +} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl new file mode 100644 index 0000000000..cbf6335fb0 --- /dev/null +++ b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl @@ -0,0 +1,20 @@ +/** + * Copyright (C) 2014 Garmin International Ltd. + * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. + */ +package com.garmin.android.connectiq; + +parcelable IQMessage { + const int SUCCESS = 0; + const int FAILURE_UNKNOWN = 1; + const int FAILURE_INVALID_FORMAT = 2; + const int FAILURE_MESSAGE_TOO_LARGE = 3; + const int FAILURE_UNSUPPORTED_TYPE = 4; + const int FAILURE_DURING_TRANSFER = 5; + const int FAILURE_INVALID_DEVICE = 6; + const int FAILURE_DEVICE_NOT_CONNECTED = 7; + + byte[] messageData; + String notificationPackage; + String notificationAction; +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt new file mode 100644 index 0000000000..3aec9257a9 --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt @@ -0,0 +1,39 @@ +package app.aaps.plugins.sync.garmin + +data class GarminApplication( + val client: GarminClient, + val device: GarminDevice, + val id: String, + val name: String?) { + + enum class Status { + @Suppress("UNUSED") + UNKNOWN, + INSTALLED, + @Suppress("UNUSED") + NOT_INSTALLED, + @Suppress("UNUSED") + NOT_SUPPORTED; + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GarminApplication + + if (client != other.client) return false + if (device != other.device) return false + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + var result = client.hashCode() + result = 31 * result + device.hashCode() + result = 31 * result + id.hashCode() + return result + } +} + diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt new file mode 100644 index 0000000000..9c1631497d --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt @@ -0,0 +1,14 @@ +package app.aaps.plugins.sync.garmin + +import io.reactivex.rxjava3.disposables.Disposable + +interface GarminClient: Disposable { + /** Name of the client. */ + val name: String + + /** Asynchronously retrieves status information for the given application. */ + fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) + + /** Asynchronously sends a message to an application. */ + fun sendMessage(app: GarminApplication, data: ByteArray) +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt new file mode 100644 index 0000000000..b0fc0ead8f --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt @@ -0,0 +1,54 @@ +package app.aaps.plugins.sync.garmin + +import com.garmin.android.connectiq.IQDevice + +data class GarminDevice( + val client: GarminClient, + val id: Long, + var name: String, + var status: Status = Status.UNKNOWN) { + + constructor(client: GarminClient, iqDevice: IQDevice): this( + client, + iqDevice.deviceIdentifier, + iqDevice.friendlyName, + Status.from(iqDevice.status)) {} + + enum class Status { + NOT_PAIRED, + NOT_CONNECTED, + CONNECTED, + UNKNOWN; + + companion object { + fun from(ordinal: Int?): Status = + values().firstOrNull { s -> s.ordinal == ordinal } ?: UNKNOWN + } + } + + + override fun toString(): String = "D[$name/$id]" + + fun toIQDevice() = IQDevice().apply { + deviceIdentifier = id + friendlyName = name + status = Status.UNKNOWN.ordinal } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GarminDevice + + if (client != other.client) return false + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + var result = client.hashCode() + result = 31 * result + id.hashCode() + return result + } +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt new file mode 100644 index 0000000000..358b7b9f16 --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt @@ -0,0 +1,286 @@ +package app.aaps.plugins.sync.garmin + +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.ServiceConnection +import android.os.IBinder +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.utils.waitMillis +import com.garmin.android.apps.connectmobile.connectiq.IConnectIQService +import com.garmin.android.connectiq.IQApp +import com.garmin.android.connectiq.IQDevice +import com.garmin.android.connectiq.IQMessage +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers +import org.jetbrains.annotations.VisibleForTesting +import java.time.Duration +import java.time.Instant +import java.util.LinkedList +import java.util.Locale +import java.util.Queue +import java.util.concurrent.TimeUnit + + +/** GarminClient that talks via the ConnectIQ app to a physical device. */ +class GarminDeviceClient( + private val aapsLogger: AAPSLogger, + private val context: Context, + private val receiver: GarminReceiver, + private val retryWaitFactor: Long = 5L): Disposable, GarminClient { + + override val name = "Device" + private var bindLock = Object() + private var ciqService: IConnectIQService? = null + get() { + val waitUntil = Instant.now().plusSeconds(2) + synchronized (bindLock) { + while(field?.asBinder()?.isBinderAlive != true) { + field = null + if (state !in arrayOf(State.BINDING, State.RECONNECTING)) { + aapsLogger.info(LTag.GARMIN, "reconnecting to ConnectIQ service") + state = State.RECONNECTING + context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE) + } + // Wait for the connection, that is the call to onServiceConnected. + val wait = Duration.between(Instant.now(), waitUntil) + if (wait > Duration.ZERO) bindLock.waitMillis(wait.toMillis()) + if (field == null) { + // The [serviceConnection] didn't have a chance to reassign ciqService, + // i.e. the wait timed out. Give up. + aapsLogger.warn(LTag.GARMIN, "no ciqservice $this") + return null + } + } + return field + } + } + + private val registeredActions = mutableSetOf() + private val broadcastReceiver = mutableListOf() + private var state = State.DISCONNECTED + private val serviceIntent get() = Intent(CONNECTIQ_SERVICE_ACTION).apply { + component = CONNECTIQ_SERVICE_COMPONENT } + + @VisibleForTesting + val sendMessageAction = createAction("SEND_MESSAGE") + + private enum class State { + BINDING, + CONNECTED, + DISCONNECTED, + DISPOSED, + RECONNECTING, + } + + private val ciqServiceConnection = object: ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + var notifyReceiver: Boolean + val ciq: IConnectIQService + synchronized (bindLock) { + aapsLogger.info(LTag.GARMIN, "ConnectIQ App connected") + ciq = IConnectIQService.Stub.asInterface(service) + notifyReceiver = state != State.RECONNECTING + state = State.CONNECTED + ciqService = ciq + bindLock.notifyAll() + } + if (notifyReceiver) receiver.onConnect(this@GarminDeviceClient) + ciq.connectedDevices?.forEach { d -> + receiver.onConnectDevice(this@GarminDeviceClient, d.deviceIdentifier, d.friendlyName) } + } + override fun onServiceDisconnected(name: ComponentName?) { + synchronized(bindLock) { + aapsLogger.info(LTag.GARMIN, "ConnectIQ App disconnected") + ciqService = null + if (state != State.DISPOSED) state = State.DISCONNECTED + } + broadcastReceiver.forEach { br -> context.unregisterReceiver(br) } + broadcastReceiver.clear() + registeredActions.clear() + receiver.onDisconnect(this@GarminDeviceClient) + } + } + + init { + aapsLogger.info(LTag.GARMIN, "binding to ConnectIQ service") + registerReceiver(sendMessageAction, ::onSendMessage) + state = State.BINDING + context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE) + } + + override fun isDisposed() = state == State.DISPOSED + override fun dispose() { + broadcastReceiver.forEach { context.unregisterReceiver(it) } + broadcastReceiver.clear() + registeredActions.clear() + try { + context.unbindService(ciqServiceConnection) + } catch (e: Exception) { + aapsLogger.warn(LTag.GARMIN, "unbind CIQ failed ${e.message}") + } + state = State.DISPOSED + } + + /** Creates a unique action name for ConnectIQ callbacks. */ + private fun createAction(action: String) = "${javaClass.`package`!!.name}.$action" + + /** Registers a callback [BroadcastReceiver] under the given action that will + * used by the ConnectIQ app for callbacks.*/ + private fun registerReceiver(action: String, receive: (intent: Intent) -> Unit) { + val recv = object: BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent) { receive(intent) } + } + broadcastReceiver.add(recv) + context.registerReceiver(recv, IntentFilter(action)) + } + + override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) { + val action = createAction("APPLICATION_INFO_${device.id}_$appId") + synchronized (registeredActions) { + if (!registeredActions.contains(action)) { + registerReceiver(action) { intent -> onApplicationInfo(appId, device, intent) } + } + registeredActions.add(action) + } + ciqService?.getApplicationInfo(context.packageName, action, device.toIQDevice(), appId) + } + + /** Receives application info callbacks from ConnectIQ app.*/ + private fun onApplicationInfo(appId: String, device: GarminDevice, intent: Intent) { + val receivedAppId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.lowercase(Locale.getDefault()) + val version = intent.getIntExtra(EXTRA_APPLICATION_VERSION, -1) + val isInstalled = receivedAppId != null && version >= 0 && version != 65535 + + if (isInstalled) registerForMessages(device.id, appId) + receiver.onApplicationInfo(device, appId, isInstalled) + } + + private fun registerForMessages(deviceId: Long, appId: String) { + aapsLogger.info(LTag.GARMIN, "registerForMessage $name $appId") + val action = createAction("ON_MESSAGE_${deviceId}_$appId") + val app = IQApp().apply { applicationID = appId; displayName = "" } + synchronized (registeredActions) { + if (!registeredActions.contains(action)) { + registerReceiver(action) { intent: Intent -> onReceiveMessage(app, intent) } + ciqService?.registerApp(app, action, context.packageName) + registeredActions.add(action) + } else { + aapsLogger.info(LTag.GARMIN, "registerForMessage $action already registered") + } + } + } + + @Suppress("Deprecation") + private fun onReceiveMessage(iqApp: IQApp, intent: Intent) { + val iqDevice = intent.getParcelableExtra(EXTRA_REMOTE_DEVICE) as IQDevice? + val data = intent.getByteArrayExtra(EXTRA_PAYLOAD) + if (iqDevice != null && data != null) + receiver.onReceiveMessage(this, iqDevice.deviceIdentifier, iqApp.applicationID, data) + } + + /** Receives callback from ConnectIQ about message transfers. */ + private fun onSendMessage(intent: Intent) { + val status = intent.getIntExtra(EXTRA_STATUS, 0) + val deviceId = getDevice(intent) + val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.lowercase() + if (deviceId == null || appId == null) { + aapsLogger.warn(LTag.GARMIN, "onSendMessage device='$deviceId' app='$appId'") + } else { + synchronized (messageQueues) { + val queue = messageQueues[deviceId to appId] + val msg = queue?.peek() + if (queue == null || msg == null) { + aapsLogger.warn(LTag.GARMIN, "onSendMessage unknown message $deviceId, $appId, $status") + return + } + + when (status) { + IQMessage.FAILURE_DEVICE_NOT_CONNECTED, + IQMessage.FAILURE_DURING_TRANSFER -> { + if (msg.attempt < MAX_RETRIES) { + val delaySec = retryWaitFactor * msg.attempt + Schedulers.io().scheduleDirect({ retryMessage(deviceId, appId) }, delaySec, TimeUnit.SECONDS) + return + } + } + + else -> {} + } + queue.remove(msg) + val errorMessage = status + .takeUnless { it == IQMessage.SUCCESS }?.let { s -> "error $s" } + receiver.onSendMessage(this, msg.app.device.id, msg.app.id, errorMessage) + if (queue.isNotEmpty()) { + Schedulers.io().scheduleDirect { retryMessage(deviceId, appId) } + } + } + } + } + + @Suppress("Deprecation") + private fun getDevice(intent: Intent): Long? { + val rawDevice = intent.extras?.get(EXTRA_REMOTE_DEVICE) + return if (rawDevice is Long) rawDevice else (rawDevice as IQDevice?)?.deviceIdentifier + ?: return null + } + + private class Message( + val app: GarminApplication, + val data: ByteArray) { + var attempt: Int = 0 + var lastAttempt: Instant? = null + val iqApp get() = IQApp().apply { applicationID = app.id; displayName = app.name } + val iqDevice get() = app.device.toIQDevice() + } + + private val messageQueues = mutableMapOf, Queue> () + + override fun sendMessage(app: GarminApplication, data: ByteArray) { + val msg = synchronized (messageQueues) { + val msg = Message(app, data) + val queue = messageQueues.getOrPut(app.device.id to app.id) { LinkedList() } + queue.add(msg) + // Make sure we have only one outstanding message per app, so we ensure + // that always the first message in the queue is currently send. + if (queue.size == 1) msg else null + } + if (msg != null) sendMessage(msg) + } + + private fun retryMessage(deviceId: Long, appId: String) { + val msg = synchronized (messageQueues) { + messageQueues[deviceId to appId]?.peek() ?: return + } + sendMessage(msg) + } + + private fun sendMessage(msg: Message) { + msg.attempt++ + msg.lastAttempt = Instant.now() + val iqMsg = IQMessage().apply { + messageData = msg.data + notificationPackage = context.packageName + notificationAction = sendMessageAction } + ciqService?.sendMessage(iqMsg, msg.iqDevice, msg.iqApp) + } + + override fun toString() = "$name[$state]" + + companion object { + const val CONNECTIQ_SERVICE_ACTION = "com.garmin.android.apps.connectmobile.CONNECTIQ_SERVICE_ACTION" + const val EXTRA_APPLICATION_ID = "com.garmin.android.connectiq.EXTRA_APPLICATION_ID" + const val EXTRA_APPLICATION_VERSION = "com.garmin.android.connectiq.EXTRA_APPLICATION_VERSION" + const val EXTRA_REMOTE_DEVICE = "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE" + const val EXTRA_PAYLOAD = "com.garmin.android.connectiq.EXTRA_PAYLOAD" + const val EXTRA_STATUS = "com.garmin.android.connectiq.EXTRA_STATUS" + val CONNECTIQ_SERVICE_COMPONENT = ComponentName( + "com.garmin.android.apps.connectmobile", + "com.garmin.android.apps.connectmobile.connectiq.ConnectIQService") + + const val MAX_RETRIES = 10 + } +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt new file mode 100644 index 0000000000..87c60ece7e --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt @@ -0,0 +1,159 @@ +package app.aaps.plugins.sync.garmin + +import android.content.Context +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import io.reactivex.rxjava3.disposables.Disposable +import org.jetbrains.annotations.VisibleForTesting + +class GarminMessenger( + private val aapsLogger: AAPSLogger, + private val context: Context, + applicationIdNames: Map, + private val messageCallback: (app: GarminApplication, msg: Any) -> Unit, + enableConnectIq: Boolean, + enableSimulator: Boolean): Disposable, GarminReceiver { + + private var disposed: Boolean = false + /** All devices that where connected since this instance was created. */ + private val devices = mutableMapOf() + @VisibleForTesting + val liveApplications = mutableSetOf() + private val clients = mutableListOf() + private val appIdNames = mutableMapOf() + init { + aapsLogger.info(LTag.GARMIN, "init CIQ debug=$enableSimulator") + appIdNames.putAll(applicationIdNames) + if (enableConnectIq) startDeviceClient() + if (enableSimulator) { + appIdNames["SimAp"] = "SimulatorApp" + GarminSimulatorClient(aapsLogger, this) + } + } + + private fun getDevice(client: GarminClient, deviceId: Long): GarminDevice { + synchronized (devices) { + return devices.getOrPut(deviceId) { GarminDevice(client, deviceId, "unknown") } + } + } + + private fun getApplication(client: GarminClient, deviceId: Long, appId: String): GarminApplication { + synchronized (liveApplications) { + var app = liveApplications.firstOrNull { app -> + app.client == client && app.device.id == deviceId && app.id == appId } + if (app == null) { + app = GarminApplication(client, getDevice(client, deviceId), appId, appIdNames[appId]) + liveApplications.add(app) + } + return app + } + } + + private fun startDeviceClient() { + GarminDeviceClient(aapsLogger, context, this) + } + + override fun onConnect(client: GarminClient) { + aapsLogger.info(LTag.GARMIN, "onConnect $client") + clients.add(client) + } + + override fun onDisconnect(client: GarminClient) { + aapsLogger.info(LTag.GARMIN, "onDisconnect ${client.name}") + clients.remove(client) + synchronized (liveApplications) { + liveApplications.removeIf { app -> app.client == client } + } + client.dispose() + when (client.name) { + "Device" -> startDeviceClient() + "Sim"-> GarminSimulatorClient(aapsLogger, this) + else -> aapsLogger.warn(LTag.GARMIN, "onDisconnect unknown client $client") + } + } + + /** Receives notifications that a device has connected. + * + * It will retrieve status information for all applications we care about (in [appIdNames]). */ + override fun onConnectDevice(client: GarminClient, deviceId: Long, deviceName: String) { + val device = getDevice(client, deviceId).apply { name = deviceName } + aapsLogger.info(LTag.GARMIN, "onConnectDevice $device") + appIdNames.forEach { (id, name) -> client.retrieveApplicationInfo(device, id, name) } + } + + /** Receives notifications about disconnection of a device. */ + override fun onDisconnectDevice(client: GarminClient, deviceId: Long) { + val device = getDevice(client, deviceId) + aapsLogger.info(LTag.GARMIN,"onDisconnectDevice $device") + synchronized (liveApplications) { + liveApplications.removeIf { app -> app.device == device } + } + } + + /** Receives notification about applications that are installed/uninstalled + * on a device from the client. */ + override fun onApplicationInfo(device: GarminDevice, appId: String, isInstalled: Boolean) { + val app = getApplication(device.client, device.id, appId) + aapsLogger.info(LTag.GARMIN, "onApplicationInfo add $app ${if (isInstalled) "" else "un"}installed") + if (!isInstalled) { + synchronized (liveApplications) { liveApplications.remove(app) } + } + } + + override fun onReceiveMessage(client: GarminClient, deviceId: Long, appId: String, data: ByteArray) { + val app = getApplication(client, deviceId, appId) + val msg = GarminSerializer.deserialize(data) + if (msg == null) { + aapsLogger.warn(LTag.GARMIN, "receive NULL msg") + } else { + aapsLogger.info(LTag.GARMIN, "receive ${data.size} bytes") + messageCallback(app, msg) + } + } + + /** Receives status notifications for a sent message. */ + override fun onSendMessage(client: GarminClient, deviceId: Long, appId: String, errorMessage: String?) { + val app = getApplication(client, deviceId, appId) + aapsLogger.info(LTag.GARMIN, "onSendMessage $app ${errorMessage ?: "OK"}") + } + + fun sendMessage(device: GarminDevice, msg: Any) { + liveApplications + .filter { a -> a.device.id == device.id } + .forEach { a -> sendMessage(a, msg) } + } + + /** Sends a message to all applications on all devices. */ + fun sendMessage(msg: Any) { + liveApplications.forEach { app -> sendMessage(app, msg) } + } + + private fun sendMessage(app: GarminApplication, msg: Any) { + // Convert msg to string for logging. + val s = when (msg) { + is Map<*,*> -> + msg.entries.joinToString(", ", "(", ")") { (k, v) -> "$k=$v" } + is List<*> -> + msg.joinToString(", ", "(", ")") + else -> + msg.toString() + } + val data = GarminSerializer.serialize(msg) + aapsLogger.info(LTag.GARMIN, "sendMessage $app $app ${data.size} bytes $s") + try { + app.client.sendMessage(app, data) + } catch (e: IllegalStateException) { + aapsLogger.error(LTag.GARMIN, "${app.client} not connected", e) + } + } + + override fun dispose() { + if (!disposed) { + clients.forEach { c -> c.dispose() } + disposed = true + } + clients.clear() + } + + override fun isDisposed() = disposed +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index 5d20fd3c70..50f9410e7e 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sync.garmin +import android.content.Context import androidx.annotation.VisibleForTesting import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.logging.AAPSLogger @@ -18,6 +19,7 @@ import com.google.gson.JsonObject import dagger.android.HasAndroidInjector import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.schedulers.Schedulers +import java.net.HttpURLConnection import java.net.SocketAddress import java.net.URI import java.time.Clock @@ -42,6 +44,7 @@ class GarminPlugin @Inject constructor( injector: HasAndroidInjector, aapsLogger: AAPSLogger, resourceHelper: ResourceHelper, + private val context: Context, private val loopHub: LoopHub, private val rxBus: RxBus, private val sp: SP, @@ -57,7 +60,19 @@ class GarminPlugin @Inject constructor( ) { /** HTTP Server for local HTTP server communication (device app requests values) .*/ private var server: HttpServer? = null + var garminMessenger: GarminMessenger? = null + /** Garmin ConnectIQ application id for native communication. Phone pushes values. */ + private val glucoseAppIds = mapOf( + "c9e90ee7e6924829a8b45e7dafff5cb4" to "GlucoseWatch_Dev", + "1107ca6c2d5644b998d4bcb3793f2b7c" to "GlucoseDataField_Dev", + "928fe19a4d3a4259b50cb6f9ddaf0f4a" to "GlucoseWidget_Dev", + "662dfcf7f5a147de8bd37f09574adb11" to "GlucoseWatch", + "815c7328c21248c493ad9ac4682fe6b3" to "GlucoseDataField", + "4bddcc1740084a1fab83a3b2e2fcf55b" to "GlucoseWidget", + ) + + @VisibleForTesting private val disposable = CompositeDisposable() @VisibleForTesting @@ -68,10 +83,25 @@ class GarminPlugin @Inject constructor( var newValue: Condition = valueLock.newCondition() private var lastGlucoseValueTimestamp: Long? = null private val glucoseUnitStr get() = if (loopHub.glucoseUnit == GlucoseUnit.MGDL) "mgdl" else "mmoll" + private val garminAapsKey get() = sp.getString("garmin_aaps_key", "") private fun onPreferenceChange(event: EventPreferenceChange) { aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}") - setupHttpServer() + when (event.changedKey) { + "communication_debug_mode" -> setupGarminMessenger() + "communication_http", "communication_http_port" -> setupHttpServer() + "garmin_aaps_key" -> sendPhoneAppMessage() + } + } + + private fun setupGarminMessenger() { + val enableDebug = sp.getBoolean("communication_ciq_debug_mode", false) + garminMessenger?.dispose() + garminMessenger = null + aapsLogger.info(LTag.GARMIN, "initialize IQ messenger in debug=$enableDebug") + garminMessenger = GarminMessenger( + aapsLogger, context, glucoseAppIds, {_, _ -> }, + true, enableDebug).also { disposable.add(it) } } override fun onStart() { @@ -83,19 +113,26 @@ class GarminPlugin @Inject constructor( .observeOn(Schedulers.io()) .subscribe(::onPreferenceChange) ) + disposable.add( + rxBus + .toObservable(EventNewBG::class.java) + .observeOn(Schedulers.io()) + .subscribe(::onNewBloodGlucose) + ) setupHttpServer() + setupGarminMessenger() } - private fun setupHttpServer() { + 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) - registerEndpoint("/carbs", ::onPostCarbs) - registerEndpoint("/connect", ::onConnectPump) + registerEndpoint("/get", requestHandler(::onGetBloodGlucose)) + registerEndpoint("/carbs", requestHandler(::onPostCarbs)) + registerEndpoint("/connect", requestHandler(::onConnectPump)) } } else if (server != null) { aapsLogger.info(LTag.GARMIN, "stopping HTTP server") @@ -104,7 +141,7 @@ class GarminPlugin @Inject constructor( } } - override fun onStop() { + public override fun onStop() { disposable.clear() aapsLogger.info(LTag.GARMIN, "Stop") server?.close() @@ -128,6 +165,34 @@ class GarminPlugin @Inject constructor( } } + @VisibleForTesting + fun onConnectDevice(device: GarminDevice) { + aapsLogger.info(LTag.GARMIN, "onConnectDevice $device sending glucose") + if (garminAapsKey.isNotEmpty()) sendPhoneAppMessage(device) + } + + private fun sendPhoneAppMessage(device: GarminDevice) { + garminMessenger?.sendMessage(device, getGlucoseMessage()) + } + + private fun sendPhoneAppMessage() { + garminMessenger?.sendMessage(getGlucoseMessage()) + } + + @VisibleForTesting + fun getGlucoseMessage() = mapOf( + "key" to garminAapsKey, + "command" to "glucose", + "profile" to loopHub.currentProfileName.first().toString(), + "encodedGlucose" to encodedGlucose(getGlucoseValues()), + "remainingInsulin" to loopHub.insulinOnboard, + "glucoseUnit" to glucoseUnitStr, + "temporaryBasalRate" to + (loopHub.temporaryBasal.takeIf(java.lang.Double::isFinite) ?: 1.0), + "connected" to loopHub.isConnected, + "timestamp" to clock.instant().epochSecond + ) + /** Gets the last 2+ hours of glucose values. */ @VisibleForTesting fun getGlucoseValues(): List { @@ -161,21 +226,33 @@ class GarminPlugin @Inject constructor( 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() } + @VisibleForTesting + fun requestHandler(action: (URI) -> CharSequence) = { + caller: SocketAddress, uri: URI, _: String? -> + val key = garminAapsKey + val deviceKey = getQueryParameter(uri, "key") + if (key.isNotEmpty() && key != deviceKey) { + aapsLogger.warn(LTag.GARMIN, "Invalid AAPS Key from $caller, got '$deviceKey' want '$key' $uri") + sendPhoneAppMessage() + Thread.sleep(1000L) + HttpURLConnection.HTTP_UNAUTHORIZED to "{}" + } else { + aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri") + HttpURLConnection.HTTP_OK to action(uri).also { + aapsLogger.info(LTag.GARMIN, "get from $caller resp , req: $uri, result: $it") + } + } + } + /** 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") + fun onGetBloodGlucose(uri: URI): CharSequence { receiveHeartRate(uri) val profileName = loopHub.currentProfileName val waitSec = getQueryParameter(uri, "wait", 0L) @@ -189,9 +266,7 @@ class GarminPlugin @Inject constructor( } 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") - } + return jo.toString() } private fun getQueryParameter(uri: URI, name: String) = (uri.query ?: "") @@ -223,6 +298,19 @@ class GarminPlugin @Inject constructor( } } + private fun toLong(v: Any?) = (v as? Number?)?.toLong() ?: 0L + + @VisibleForTesting + fun receiveHeartRate(msg: Map, test: Boolean) { + val avg: Int = msg.getOrDefault("hr", 0) as Int + val samplingStartSec: Long = toLong(msg["hrStart"]) + val samplingEndSec: Long = toLong(msg["hrEnd"]) + val device: String? = msg["device"] as String? + receiveHeartRate( + Instant.ofEpochSecond(samplingStartSec), Instant.ofEpochSecond(samplingEndSec), + avg, device, test) + } + @VisibleForTesting fun receiveHeartRate(uri: URI) { val avg: Int = getQueryParameter(uri, "hr", 0L).toInt() @@ -237,7 +325,7 @@ class GarminPlugin @Inject constructor( private fun receiveHeartRate( samplingStart: Instant, samplingEnd: Instant, avg: Int, device: String?, test: Boolean) { - aapsLogger.info(LTag.GARMIN, "average heart rate $avg BPM test=$test") + aapsLogger.info(LTag.GARMIN, "average heart rate $avg BPM $samplingStart to $samplingEnd") if (test) return if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) { loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device) @@ -248,9 +336,7 @@ class GarminPlugin @Inject constructor( /** Handles carb notification from the device. */ @VisibleForTesting - @Suppress("UNUSED_PARAMETER") - fun onPostCarbs(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence { - aapsLogger.info(LTag.GARMIN, "carbs from $caller, req: $uri") + fun onPostCarbs(uri: URI): CharSequence { postCarbs(getQueryParameter(uri, "carbs", 0L).toInt()) return "" } @@ -263,9 +349,7 @@ class GarminPlugin @Inject constructor( /** Handles pump connected notification that the user entered on the Garmin device. */ @VisibleForTesting - @Suppress("UNUSED_PARAMETER") - fun onConnectPump(caller: SocketAddress, uri: URI, requestBody: String?): CharSequence { - aapsLogger.info(LTag.GARMIN, "connect from $caller, req: $uri") + fun onConnectPump(uri: URI): CharSequence { val minutes = getQueryParameter(uri, "disconnectMinutes", 0L).toInt() if (minutes > 0) { loopHub.disconnectPump(minutes) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt new file mode 100644 index 0000000000..c308e4bead --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt @@ -0,0 +1,36 @@ +package app.aaps.plugins.sync.garmin + +/** + * Callback interface for a @see ConnectIqClient. + */ +interface GarminReceiver { + /** + * Notifies that the client is ready, i.e. the app client as bound to the Garmin + * Android app. + */ + fun onConnect(client: GarminClient) + fun onDisconnect(client: GarminClient) + + /** + * Notifies that a device is connected. This will be called for all connected devices + * initially. + */ + fun onConnectDevice(client: GarminClient, deviceId: Long, deviceName: String) + fun onDisconnectDevice(client: GarminClient, deviceId: Long) + + /** + * Provides application info after a call to + * {@link ConnectIqClient#retrieveApplicationInfo retrieveApplicationInfo}. + */ + fun onApplicationInfo(device: GarminDevice, appId: String, isInstalled: Boolean) + + /** + * Delivers received device app messages. + */ + fun onReceiveMessage(client: GarminClient, deviceId: Long, appId: String, data: ByteArray) + + /** + * Delivers status of @see ConnectIqClient#sendMessage requests. + */ + fun onSendMessage(client: GarminClient, deviceId: Long, appId: String, errorMessage: String?) +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSerializer.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSerializer.kt new file mode 100644 index 0000000000..495278e9da --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSerializer.kt @@ -0,0 +1,254 @@ +package app.aaps.plugins.sync.garmin + +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +import java.nio.ByteBuffer +import java.util.ArrayDeque +import java.util.Queue + +/** + * Serialize and Deserialize objects in Garmin format. + * + * Format is as follows: + * ... + * + * Serialized data starts with an optional string block. The string block is preceded with the STRS_MARKER, + * followed by the total length of the reminder (4 bytes). Then foreach string, the string length + * (2 bytes), followed by the string bytes, followed by a \0 byte. + * + * Objects are stored starting with OBJS_MARKER, followed by the total length (4 bytes), followed + * by a flat list of objects. Each object starts with its type (1 byte), followed by the data + * for numbers in Boolean. Strings a represented by an index into the string block. Arrays only have + * the length, the actual objects will be in the list of objects. Similarly, maps only have the + * length and the entries are represented by 2 objects (key + val) in the list of objects. + */ +object GarminSerializer { + private const val NULL = 0 + private const val INT = 1 + private const val FLOAT = 2 + private const val STRING = 3 + private const val ARRAY = 5 + private const val BOOLEAN = 9 + private const val MAP = 11 + private const val LONG = 14 + private const val DOUBLE = 15 + private const val CHAR = 19 + + private const val STRINGS_MARKER = -1412584499 + private const val OBJECTS_MARKER = -629482886 + // ArrayDeque doesn't like null so we use this instead. + private val NULL_MARKER = object {} + + private interface Container { + fun read(buf: ByteBuffer, strings: Map, container: Queue) + } + + private class ListContainer( + val size: Int, + val list: MutableList + ) : Container { + + override fun read(buf: ByteBuffer, strings: Map, container: Queue) { + for (i in 0 until size) { + list.add(readObject(buf, strings, container)) + } + } + } + + private class MapContainer( + val size: Int, + val map: MutableMap + ) : Container { + + override fun read(buf: ByteBuffer, strings: Map, container: Queue) { + for (i in 0 until size) { + val k = readObject(buf, strings, container) + val v = readObject(buf, strings, container) + map[k!!] = v + } + } + } + + + fun serialize(obj: Any?): ByteArray { + val strsOut = ByteArrayOutputStream() + val strsDataOut = DataOutputStream(strsOut) + val objsOut = ByteArrayOutputStream() + val strings = mutableMapOf() + val q = ArrayDeque() + + q.add(obj ?: NULL_MARKER) + while (!q.isEmpty()) { + serialize(q.poll(), strsDataOut, DataOutputStream(objsOut), strings, q) + } + + var bufLen = 8 + objsOut.size() + if (strsOut.size() > 0) { + bufLen += 8 + strsOut.size() + } + + val buf = ByteBuffer.allocate(bufLen) + if (strsOut.size() > 0) { + buf.putInt(STRINGS_MARKER) + buf.putInt(strsOut.size()) + buf.put(strsOut.toByteArray(), 0, strsOut.size()) + } + buf.putInt(OBJECTS_MARKER) + buf.putInt(objsOut.size()) + buf.put(objsOut.toByteArray(), 0, objsOut.size()) + return buf.array() + } + + private fun serialize( + obj: Any?, + strOut: DataOutputStream, + objOut: DataOutputStream, + strings: MutableMap, + q: Queue + ) { + when (obj) { + NULL_MARKER -> objOut.writeByte(NULL) + + is Int -> { + objOut.writeByte(INT) + objOut.writeInt(obj) + } + + is Float -> { + objOut.writeByte(FLOAT) + objOut.writeFloat(obj) + } + + is String -> { + objOut.writeByte(STRING) + val offset = strings[obj] + if (offset == null) { + strings[obj] = strOut.size() + val bytes = obj.toByteArray(Charsets.UTF_8) + strOut.writeShort(bytes.size + 1) + strOut.write(bytes) + strOut.write(0) + } + objOut.writeInt(strings[obj]!!) + } + + is List<*> -> { + objOut.writeByte(ARRAY) + objOut.writeInt(obj.size) + obj.forEach { o -> q.add(o ?: NULL_MARKER) } + } + + is Boolean -> { + objOut.writeByte(BOOLEAN) + objOut.writeByte(if (obj) 1 else 0) + } + + is Map<*, *> -> { + objOut.writeByte(MAP) + objOut.writeInt(obj.size) + obj.entries.forEach { (k, v) -> + q.add(k ?: NULL_MARKER); q.add(v ?: NULL_MARKER) } + } + + is Long -> { + objOut.writeByte(LONG) + objOut.writeLong(obj) + } + + is Double -> { + objOut.writeByte(DOUBLE) + objOut.writeDouble(obj) + } + + is Char -> { + objOut.writeByte(CHAR) + objOut.writeInt(obj.code) + } + + else -> + throw IllegalArgumentException("Unsupported type ${obj?.javaClass} '$obj'") + } + } + + fun deserialize(data: ByteArray): Any? { + val buf = ByteBuffer.wrap(data) + val marker1 = buf.getInt(0) + val strings = if (marker1 == STRINGS_MARKER) { + buf.int // swallow the marker + readStrings(buf) + } else { + emptyMap() + } + val marker2 = buf.int // swallow the marker + if (marker2 != OBJECTS_MARKER) { + throw IllegalArgumentException("expected data marker, got $marker2") + } + return readObjects(buf, strings) + } + + private fun readStrings(buf: ByteBuffer): Map { + val strings = mutableMapOf() + val strBufferLen = buf.int + val offset = buf.position() + while (buf.position() - offset < strBufferLen) { + val pos = buf.position() - offset + val strLen = buf.short.toInt() - 1 // ignore \0 byte + val strBytes = ByteArray(strLen) + buf.get(strBytes) + strings[pos] = String(strBytes, Charsets.UTF_8) + buf.get() // swallow \0 byte + } + return strings + } + + private fun readObjects(buf: ByteBuffer, strings: Map): Any? { + val objBufferLen = buf.int + if (objBufferLen > buf.remaining()) { + throw IllegalArgumentException("expect $objBufferLen bytes got ${buf.remaining()}") + } + + val container = ArrayDeque() + val r = readObject(buf, strings, container) + while (container.isNotEmpty()) { + container.pollFirst()?.read(buf, strings, container) + } + + return r + } + + private fun readObject(buf: ByteBuffer, strings: Map, q: Queue): Any? { + when (buf.get().toInt()) { + NULL -> return null + INT -> return buf.int + FLOAT -> return buf.float + + STRING -> { + val offset = buf.int + return strings[offset]!! + } + + ARRAY -> { + val arraySize = buf.int + val array = mutableListOf() + // We will populate the array with arraySize objects from the object list later, + // when we take the ListContainer from the queue. + q.add(ListContainer(arraySize, array)) + return array + } + + BOOLEAN -> return buf.get() > 0 + + MAP -> { + val mapSize = buf.int + val map = mutableMapOf() + q.add(MapContainer(mapSize, map)) + return map + } + + LONG -> return buf.long + DOUBLE -> return buf.double + CHAR -> return Char(buf.int) + else -> return null + } + } +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt new file mode 100644 index 0000000000..f473773a2a --- /dev/null +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt @@ -0,0 +1,186 @@ +package app.aaps.plugins.sync.garmin + +import app.aaps.core.interfaces.logging.AAPSLogger +import app.aaps.core.interfaces.logging.LTag +import com.garmin.android.connectiq.IQApp +import io.reactivex.rxjava3.disposables.Disposable +import org.jetbrains.annotations.VisibleForTesting +import java.io.InputStream +import java.net.Inet4Address +import java.net.InetSocketAddress +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketException +import java.time.Duration +import java.util.Collections +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** [GarminClient] that talks to the ConnectIQ simulator via HTTP. + * + * This is needed for Garmin device app development. */ +class GarminSimulatorClient( + private val aapsLogger: AAPSLogger, + private val receiver: GarminReceiver, + var port: Int = 7381 +): Disposable, GarminClient { + + override val name = "Sim" + private val executor: ExecutorService = Executors.newCachedThreadPool() + private val serverSocket = ServerSocket() + private val connections: MutableList = Collections.synchronizedList(mutableListOf()) + private var nextDeviceId = AtomicLong(1) + @VisibleForTesting + val iqApp = IQApp().apply { + applicationID = "SimApp" + status = GarminApplication.Status.INSTALLED.ordinal + displayName = "Simulator" + version = 1 } + private val readyLock = ReentrantLock() + private val readyCond = readyLock.newCondition() + + private inner class Connection(private val socket: Socket): Disposable { + val device = GarminDevice( + this@GarminSimulatorClient, + nextDeviceId.getAndAdd(1L), + "Sim@${socket.remoteSocketAddress}", + GarminDevice.Status.CONNECTED) + + fun start() { + executor.execute { + try { + receiver.onConnectDevice(this@GarminSimulatorClient, device.id, device.name) + run() + } catch (e: Throwable) { + aapsLogger.error(LTag.GARMIN, "$device failed", e) + } + } + } + + fun send(data: ByteArray) { + if (socket.isConnected && !socket.isOutputShutdown) { + aapsLogger.info(LTag.GARMIN, "sending ${data.size} bytes to $device") + socket.outputStream.write(data) + socket.outputStream.flush() + } else { + aapsLogger.warn(LTag.GARMIN, "socket closed, cannot send $device") + } + } + + private fun run() { + socket.soTimeout = 0 + socket.isInputShutdown + while (!socket.isClosed && socket.isConnected) { + try { + val data = readAvailable(socket.inputStream) ?: break + if (data.isNotEmpty()) { + kotlin.runCatching { + receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationID, data) + } + } + } catch (e: SocketException) { + aapsLogger.warn(LTag.GARMIN, "socket read failed ${e.message}") + } + } + aapsLogger.info(LTag.GARMIN, "disconnect ${device.name}" ) + connections.remove(this) + receiver.onDisconnectDevice(this@GarminSimulatorClient, device.id) + } + + private fun readAvailable(input: InputStream): ByteArray? { + val buffer = ByteArray(1 shl 14) + aapsLogger.info(LTag.GARMIN, "$device reading") + val len = input.read(buffer) + aapsLogger.info(LTag.GARMIN, "$device read $len bytes") + if (len < 0) { + return null + } + val data = ByteArray(len) + System.arraycopy(buffer, 0, data, 0, data.size) + return data + } + + override fun dispose() { + aapsLogger.info(LTag.GARMIN, "close $device") + + @Suppress("EmptyCatchBlock") + try { + socket.close() + } catch (e: SocketException) { + aapsLogger.warn(LTag.GARMIN, "closing socket failed ${e.message}") + } + } + + override fun isDisposed() = socket.isClosed + } + + init { + executor.execute { + runCatching(::listen).exceptionOrNull()?.let { e-> + aapsLogger.error(LTag.GARMIN, "listen failed", e) + } + } + } + + private fun listen() { + val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) + aapsLogger.info(LTag.GARMIN, "bind to $ip:$port") + serverSocket.bind(InetSocketAddress(ip, port)) + port = serverSocket.localPort + receiver.onConnect(this@GarminSimulatorClient) + while (!serverSocket.isClosed) { + val s = serverSocket.accept() + aapsLogger.info(LTag.GARMIN, "accept " + s.remoteSocketAddress) + connections.add(Connection(s)) + connections.last().start() + } + receiver.onDisconnect(this@GarminSimulatorClient) + } + + /** Wait for the server to start listing to requests. */ + fun awaitReady(wait: Duration): Boolean { + var waitNanos = wait.toNanos() + readyLock.withLock { + while (!serverSocket.isBound && waitNanos > 0L) { + waitNanos = readyCond.awaitNanos(waitNanos) + } + } + return serverSocket.isBound + } + + override fun dispose() { + connections.forEach { c -> c.dispose() } + connections.clear() + serverSocket.close() + executor.awaitTermination(10, TimeUnit.SECONDS) + } + + override fun isDisposed() = serverSocket.isClosed + + override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) { + receiver.onApplicationInfo(device, appId, true) + } + + private fun getConnection(device: GarminDevice): Connection? { + return connections.firstOrNull { c -> c.device.id == device.id } + } + + override fun sendMessage(app: GarminApplication, data: ByteArray) { + val c = getConnection(app.device) ?: return + try { + c.send(data) + receiver.onSendMessage(this, app.device.id, app.id, null) + } catch (e: SocketException) { + val errorMessage = "sending failed '${e.message}'" + receiver.onSendMessage(this, app.device.id, app.id, errorMessage) + c.dispose() + connections.remove(c) + } + } + + override fun toString() = name +} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt index c349e3cb3d..82f2e0806d 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/HttpServer.kt @@ -34,7 +34,7 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po private val serverThread: Thread private val workerExecutor: Executor = Executors.newCachedThreadPool() - private val endpoints: MutableMap CharSequence> = + private val endpoints: MutableMap Pair> = ConcurrentHashMap() private var serverSocket: ServerSocket? = null private val readyLock = ReentrantLock() @@ -76,7 +76,7 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po } /** Register an endpoint (path) to handle requests. */ - fun registerEndpoint(path: String, endpoint: (SocketAddress, URI, String?) -> CharSequence) { + fun registerEndpoint(path: String, endpoint: (SocketAddress, URI, String?) -> Pair) { aapsLogger.info(LTag.GARMIN, "Register: '$path'") endpoints[path] = endpoint } @@ -127,8 +127,8 @@ class HttpServer internal constructor(private var aapsLogger: AAPSLogger, val po respond(HttpURLConnection.HTTP_NOT_FOUND, out) } else { try { - val body = endpoint(s.remoteSocketAddress, uri, reqBody) - respond(HttpURLConnection.HTTP_OK, body, "application/json", out) + val (code, body) = endpoint(s.remoteSocketAddress, uri, reqBody) + respond(code, body, "application/json", out) } catch (e: Exception) { aapsLogger.error(LTag.GARMIN, "endpoint " + uri.path + " failed", e) respond(HttpURLConnection.HTTP_INTERNAL_ERROR, out) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt new file mode 100644 index 0000000000..52962aebcb --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt @@ -0,0 +1,142 @@ +package app.aaps.plugins.sync.garmin + +import android.content.Context +import app.aaps.shared.tests.TestBase +import org.junit.jupiter.api.AfterEach +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.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.LinkedList +import java.util.Queue + +class GarminMessengerTest: TestBase() { + private val context = mock() + private val client1 = mock() { + on { name } doReturn "Mock1" + } + private val client2 = mock() { + on { name } doReturn "Mock2" + } + private var appId1 = "appId1" + private val appId2 = "appId2" + + private val apps = mapOf(appId1 to "$appId1-name", appId2 to "$appId2-name") + private val msgs: Queue> = LinkedList() + private var messenger = GarminMessenger( + aapsLogger, context, apps, { app, msg -> msgs.add(app to msg) }, false, false) + private val deviceId = 11L + private val deviceName = "$deviceId-name" + private val device = GarminDevice(client1, deviceId, deviceName) + private val device2 = GarminDevice(client2, 12L, "dev2-name") + + @BeforeEach + fun setup() { + messenger.onConnect(client1) + messenger.onConnect(client2) + } + @AfterEach + fun cleanup() { + messenger.dispose() + assertTrue(messenger.isDisposed) + } + + @Test + fun onConnectDevice() { + messenger.onConnectDevice(client1, deviceId, deviceName) + verify(client1).retrieveApplicationInfo(device, appId1, apps[appId1]!!) + verify(client1).retrieveApplicationInfo(device, appId2, apps[appId2]!!) + } + + @Test + fun onApplicationInfo() { + messenger.onApplicationInfo(device, appId1, true) + val app = messenger.liveApplications.first() + assertEquals(device, app.device) + assertEquals(appId1, app.id) + assertEquals(apps[appId1], app.name) + + messenger.onApplicationInfo(device, appId1, false) + assertEquals(0, messenger.liveApplications.size) + } + + @Test + fun onDisconnectDevice() { + messenger.onConnectDevice(client1, deviceId, deviceName) + messenger.onApplicationInfo(device, appId1, true) + messenger.onApplicationInfo(device2, appId1, true) + assertEquals(2, messenger.liveApplications.size) + messenger.onDisconnectDevice(client1, device2.id) + assertEquals(1, messenger.liveApplications.size) + assertEquals(appId1, messenger.liveApplications.first().id) + } + + @Test + fun onDisconnect() { + messenger.onApplicationInfo(device, appId1, true) + messenger.onApplicationInfo(device2, appId2, true) + assertEquals(2, messenger.liveApplications.size) + messenger.onDisconnect(client1) + assertEquals(1, messenger.liveApplications.size) + val app = messenger.liveApplications.first() + assertEquals(device2, app.device) + assertEquals(appId2, app.id) + assertEquals(apps[appId2], app.name) + } + + @Test + fun onReceiveMessage() { + val data = GarminSerializer.serialize("foo") + messenger.onReceiveMessage(client1, device.id, appId1, data) + val (app, payload) = msgs.remove() + assertEquals(appId1, app.id) + assertEquals("foo", payload) + } + + @Test + fun sendMessageDevice() { + messenger.onApplicationInfo(device, appId1, true) + messenger.onApplicationInfo(device, appId2, true) + + val msgs = mutableListOf>() + whenever(client1.sendMessage(any(), any())).thenAnswer { i -> + msgs.add(i.getArgument(0) to i.getArgument(1)) + } + + messenger.sendMessage(device, "foo") + assertEquals(2, msgs.size) + val msg1 = msgs.first { (app, _) -> app.id == appId1 }.second + val msg2 = msgs.first { (app, _) -> app.id == appId2 }.second + assertEquals("foo", GarminSerializer.deserialize(msg1)) + assertEquals("foo", GarminSerializer.deserialize(msg2)) + messenger.onSendMessage(client1, device.id, appId1, null) + } + + @Test + fun onSendMessageAll() { + messenger.onApplicationInfo(device, appId1, true) + messenger.onApplicationInfo(device2, appId2, true) + assertEquals(2, messenger.liveApplications.size) + + val msgs = mutableListOf>() + whenever(client1.sendMessage(any(), any())).thenAnswer { i -> + msgs.add(i.getArgument(0) to i.getArgument(1)) + } + whenever(client2.sendMessage(any(), any())).thenAnswer { i -> + msgs.add(i.getArgument(0) to i.getArgument(1)) + } + + messenger.sendMessage(listOf("foo")) + assertEquals(2, msgs.size) + val msg1 = msgs.first { (app, _) -> app.id == appId1 }.second + val msg2 = msgs.first { (app, _) -> app.id == appId2 }.second + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg1)) + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg2)) + messenger.onSendMessage(client1, device.id, appId1, null) + } +} \ No newline at end of file diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt index 18c772877e..d60ab839d2 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt @@ -1,5 +1,6 @@ package app.aaps.plugins.sync.garmin +import android.content.Context import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.rx.events.EventNewBG @@ -11,9 +12,15 @@ import dagger.android.HasAndroidInjector 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.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.atMost import org.mockito.Mockito.mock @@ -21,6 +28,8 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` +import java.net.ConnectException +import java.net.HttpURLConnection import java.net.SocketAddress import java.net.URI import java.time.Clock @@ -34,6 +43,7 @@ class GarminPluginTest: TestBase() { @Mock private lateinit var rh: ResourceHelper @Mock private lateinit var sp: SP + @Mock private lateinit var context: Context @Mock private lateinit var loopHub: LoopHub private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC")) @@ -44,9 +54,14 @@ class GarminPluginTest: TestBase() { @BeforeEach fun setup() { - gp = GarminPlugin(injector, aapsLogger, rh, loopHub, rxBus, sp) + gp = GarminPlugin(injector, aapsLogger, rh, context, loopHub, rxBus, sp) gp.clock = clock `when`(loopHub.currentProfileName).thenReturn("Default") + `when`(sp.getBoolean(anyString(), anyBoolean())).thenAnswer { i -> i.arguments[1] } + `when`(sp.getString(anyString(), anyString())).thenAnswer { i -> i.arguments[1] } + `when`(sp.getInt(anyString(), anyInt())).thenAnswer { i -> i.arguments[1] } + `when`(sp.getInt(eq("communication_http_port") ?: "", anyInt())) + .thenReturn(28890) } @AfterEach @@ -76,6 +91,17 @@ class GarminPluginTest: TestBase() { sourceSensor = GlucoseValue.SourceSensor.RANDOM ) + @Test + fun testReceiveHeartRateMap() { + val hr = createHeartRate(80) + gp.receiveHeartRate(hr, false) + verify(loopHub).storeHeartRate( + Instant.ofEpochSecond(hr["hrStart"] as Long), + Instant.ofEpochSecond(hr["hrEnd"] as Long), + 80, + hr["device"] as String) + } + @Test fun testReceiveHeartRateUri() { val hr = createHeartRate(99) @@ -119,6 +145,132 @@ class GarminPluginTest: TestBase() { verify(loopHub).getGlucoseValues(from, true) } + @Test + fun setupHttpServer_enabled() { + `when`(sp.getBoolean("communication_http", false)).thenReturn(true) + `when`(sp.getInt("communication_http_port", 28891)).thenReturn(28892) + gp.setupHttpServer() + val reqUri = URI("http://127.0.0.1:28892/get") + val resp = reqUri.toURL().openConnection() as HttpURLConnection + assertEquals(200, resp.responseCode) + + // Change port + `when`(sp.getInt("communication_http_port", 28891)).thenReturn(28893) + gp.setupHttpServer() + val reqUri2 = URI("http://127.0.0.1:28893/get") + val resp2 = reqUri2.toURL().openConnection() as HttpURLConnection + assertEquals(200, resp2.responseCode) + + `when`(sp.getBoolean("communication_http", false)).thenReturn(false) + gp.setupHttpServer() + assertThrows(ConnectException::class.java) { + (reqUri2.toURL().openConnection() as HttpURLConnection).responseCode + } + gp.onStop() + + verify(loopHub, times(2)).getGlucoseValues(anyObject(), eq(true)) + verify(loopHub, times(2)).insulinOnboard + verify(loopHub, times(2)).temporaryBasal + verify(loopHub, times(2)).isConnected + verify(loopHub, times(2)).glucoseUnit + } + + @Test + fun setupHttpServer_disabled() { + gp.setupHttpServer() + val reqUri = URI("http://127.0.0.1:28890/get") + assertThrows(ConnectException::class.java) { + (reqUri.toURL().openConnection() as HttpURLConnection).responseCode + } + } + + @Test + fun requestHandler_NoKey() { + val uri = createUri(emptyMap()) + val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" } + assertEquals( + HttpURLConnection.HTTP_OK to "OK", + handler(mock(SocketAddress::class.java), uri, null)) + } + + @Test + fun requestHandler_KeyProvided() { + val uri = createUri(mapOf("key" to "foo")) + val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" } + assertEquals( + HttpURLConnection.HTTP_OK to "OK", + handler(mock(SocketAddress::class.java), uri, null)) + } + + @Test + fun requestHandler_KeyRequiredAndProvided() { + `when`(sp.getString("garmin_aaps_key", "")).thenReturn("foo") + val uri = createUri(mapOf("key" to "foo")) + val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" } + assertEquals( + HttpURLConnection.HTTP_OK to "OK", + handler(mock(SocketAddress::class.java), uri, null)) + + } + + @Test + fun requestHandler_KeyRequired() { + gp.garminMessenger = mock(GarminMessenger::class.java) + + `when`(sp.getString("garmin_aaps_key", "")).thenReturn("foo") + val uri = createUri(emptyMap()) + val handler = gp.requestHandler { u: URI -> assertEquals(uri, u); "OK" } + assertEquals( + HttpURLConnection.HTTP_UNAUTHORIZED to "{}", + handler(mock(SocketAddress::class.java), uri, null)) + + val captor = ArgumentCaptor.forClass(Any::class.java) + verify(gp.garminMessenger)!!.sendMessage(captor.capture() ?: "") + @Suppress("UNCHECKED_CAST") + val r = captor.value as Map + assertEquals("foo", r["key"]) + assertEquals("glucose", r["command"]) + assertEquals("D", r["profile"]) + assertEquals("", r["encodedGlucose"]) + assertEquals(0.0, r["remainingInsulin"]) + assertEquals("mmoll", r["glucoseUnit"]) + assertEquals(0.0, r["temporaryBasalRate"]) + assertEquals(false, r["connected"]) + assertEquals(clock.instant().epochSecond, r["timestamp"]) + verify(loopHub).getGlucoseValues(getGlucoseValuesFrom, true) + verify(loopHub).insulinOnboard + verify(loopHub).temporaryBasal + verify(loopHub).isConnected + verify(loopHub).glucoseUnit + } + + @Test + fun onConnectDevice() { + gp.garminMessenger = mock(GarminMessenger::class.java) + `when`(sp.getString("garmin_aaps_key", "")).thenReturn("foo") + val device = GarminDevice(mock(),1, "Edge") + gp.onConnectDevice(device) + + val captor = ArgumentCaptor.forClass(Any::class.java) + verify(gp.garminMessenger)!!.sendMessage(eq(device) ?: device, captor.capture() ?: "") + @Suppress("UNCHECKED_CAST") + val r = captor.value as Map + assertEquals("foo", r["key"]) + assertEquals("glucose", r["command"]) + assertEquals("D", r["profile"]) + assertEquals("", r["encodedGlucose"]) + assertEquals(0.0, r["remainingInsulin"]) + assertEquals("mmoll", r["glucoseUnit"]) + assertEquals(0.0, r["temporaryBasalRate"]) + assertEquals(false, r["connected"]) + assertEquals(clock.instant().epochSecond, r["timestamp"]) + verify(loopHub).getGlucoseValues(getGlucoseValuesFrom, true) + verify(loopHub).insulinOnboard + verify(loopHub).temporaryBasal + verify(loopHub).isConnected + verify(loopHub).glucoseUnit + } + @Test fun testOnGetBloodGlucose() { `when`(loopHub.isConnected).thenReturn(true) @@ -129,7 +281,7 @@ class GarminPluginTest: TestBase() { listOf(createGlucoseValue(Instant.ofEpochSecond(1_000)))) val hr = createHeartRate(99) val uri = createUri(hr) - val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null) + val result = gp.onGetBloodGlucose(uri) assertEquals( "{\"encodedGlucose\":\"0A+6AQ==\"," + "\"remainingInsulin\":3.14," + @@ -161,7 +313,7 @@ class GarminPluginTest: TestBase() { params["wait"] = 10 val uri = createUri(params) gp.newValue = mock(Condition::class.java) - val result = gp.onGetBloodGlucose(mock(SocketAddress::class.java), uri, null) + val result = gp.onGetBloodGlucose(uri) assertEquals( "{\"encodedGlucose\":\"/wS6AQ==\"," + "\"remainingInsulin\":3.14," + @@ -184,7 +336,7 @@ class GarminPluginTest: TestBase() { @Test fun testOnPostCarbs() { val uri = createUri(mapOf("carbs" to "12")) - assertEquals("", gp.onPostCarbs(mock(SocketAddress::class.java), uri, null)) + assertEquals("", gp.onPostCarbs(uri)) verify(loopHub).postCarbs(12) } @@ -192,7 +344,7 @@ class GarminPluginTest: TestBase() { fun testOnConnectPump_Disconnect() { val uri = createUri(mapOf("disconnectMinutes" to "20")) `when`(loopHub.isConnected).thenReturn(false) - assertEquals("{\"connected\":false}", gp.onConnectPump(mock(SocketAddress::class.java), uri, null)) + assertEquals("{\"connected\":false}", gp.onConnectPump(uri)) verify(loopHub).disconnectPump(20) verify(loopHub).isConnected } @@ -201,7 +353,7 @@ class GarminPluginTest: TestBase() { fun testOnConnectPump_Connect() { val uri = createUri(mapOf("disconnectMinutes" to "0")) `when`(loopHub.isConnected).thenReturn(true) - assertEquals("{\"connected\":true}", gp.onConnectPump(mock(SocketAddress::class.java), uri, null)) + assertEquals("{\"connected\":true}", gp.onConnectPump(uri)) verify(loopHub).connectPump() verify(loopHub).isConnected } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSerializerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSerializerTest.kt new file mode 100644 index 0000000000..fed7e32a28 --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSerializerTest.kt @@ -0,0 +1,92 @@ +package app.aaps.plugins.sync.garmin + + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import kotlin.test.assertContentEquals + +class GarminSerializerTest { + + @Test fun testSerializeDeserializeString() { + val o = "Hello, world!" + val data = GarminSerializer.serialize(o) + assertContentEquals( + byteArrayOf( + -85, -51, -85, -51, 0, 0, 0, 16, 0, 14, 72, 101, 108, 108, 111, 44, 32, 119, 111, + 114, 108, 100, 33, 0, -38, 122, -38, 122, 0, 0, 0, 5, 3, 0,0, 0, 0), + data) + assertEquals(o, GarminSerializer.deserialize(data)) + } + + @Test fun testSerializeDeserializeInteger() { + val o = 3 + val data = GarminSerializer.serialize(o) + assertContentEquals( + byteArrayOf(-38, 122, -38, 122, 0, 0, 0, 5, 1, 0, 0, 0, 3), + data) + assertEquals(o, GarminSerializer.deserialize(data)) + } + + @Test fun tesSerializeDeserializeArray() { + val o = listOf("a", "b", true, 3, 3.4F, listOf(5L, 9), 42) + val data = GarminSerializer.serialize(o) + assertContentEquals( + byteArrayOf( + -85, -51, -85, -51, 0, 0, 0, 8, 0, 2, 97, 0, 0, 2, 98, 0, -38, 122, -38, 122, 0, 0, + 0, 55, 5, 0, 0, 0, 7, 3, 0, 0, 0, 0, 3, 0, 0, 0, 4, 9, 1, 1, 0, 0, 0, 3, 2, 64, 89, + -103, -102, 5, 0, 0, 0, 2, 1, 0, 0, 0, 42, 14, 0, 0, 0, 0, 0, 0, 0, 5, 14, 0, 0, 0, + 0, 0, 0, 0, 9), + data) + assertEquals(o, GarminSerializer.deserialize(data)) + } + + @Test + fun testSerializeDeserializeMap() { + val o = mapOf("a" to "abc", "c" to 3, "d" to listOf(4, 9, "abc"), true to null) + val data = GarminSerializer.serialize(o) + assertContentEquals( + byteArrayOf( + -85, -51, -85, -51, 0, 0, 0, 18, 0, 2, 97, 0, 0, 4, 97, 98, 99, 0, 0, 2, 99, 0, 0, + 2, 100, 0, -38, 122, -38, 122, 0, 0, 0, 53, 11, 0, 0, 0, 4, 3, 0, 0, 0, 0, 3, 0, 0, + 0, 4, 3, 0, 0, 0, 10, 1, 0, 0, 0, 3, 3, 0, 0, 0, 14, 5, 0, 0, 0, 3, 9, 1, 0, 1, 0, 0, + 0, 4, 1, 0, 0, 0, 9, 3, 0, 0, 0, 4), + data) + assertEquals(o, GarminSerializer.deserialize(data)) + } + + @Test fun testSerializeDeserializeNull() { + val o = null + val data = GarminSerializer.serialize(o) + assertContentEquals( + byteArrayOf(-38, 122, -38, 122, 0, 0, 0, 1, 0), + data) + assertEquals(o, GarminSerializer.deserialize(data)) + assertEquals(o, GarminSerializer.deserialize(data)) + } + + @Test fun testSerializeDeserializeAllPrimitiveTypes() { + val o = listOf(1, 1.2F, 1.3, "A", true, 2L, 'X', null) + val data = GarminSerializer.serialize(o) + assertContentEquals( + byteArrayOf( + -85, -51, -85, -51, 0, 0, 0, 4, 0, 2, 65, 0, -38, 122, -38, 122, 0, 0, 0, 46, 5, 0, + 0, 0, 8, 1, 0, 0, 0, 1, 2, 63, -103, -103, -102, 15, 63, -12, -52, -52, -52, -52, + -52, -51, 3, 0, 0, 0, 0, 9, 1, 14, 0, 0, 0, 0, 0, 0, 0, 2, 19, 0, 0, 0, 88, 0), + data) + assertEquals(o, GarminSerializer.deserialize(data)) + assertEquals(o, GarminSerializer.deserialize(data)) + } + + @Test fun testSerializeDeserializeMapNested() { + val o = mapOf("a" to "abc", "c" to 3, "d" to listOf(4, 9, "abc")) + val data = GarminSerializer.serialize(o) + assertContentEquals( + byteArrayOf( + -85, -51, -85, -51, 0, 0, 0, 18, 0, 2, 97, 0, 0, 4, 97, 98, 99, 0, 0, 2, 99, 0, 0, + 2, 100, 0, -38, 122, -38, 122, 0, 0, 0, 50, 11, 0, 0, 0, 3, 3, 0, 0, 0, 0, 3, 0, 0, + 0, 4, 3, 0, 0, 0, 10, 1, 0, 0, 0, 3, 3, 0, 0, 0, 14, 5, 0, 0, 0, 3, 1, 0, 0, 0, 4, + 1, 0, 0, 0, 9, 3, 0, 0, 0, 4), + data) + assertEquals(o, GarminSerializer.deserialize(data)) + } +} \ No newline at end of file diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt new file mode 100644 index 0000000000..a4b422616b --- /dev/null +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt @@ -0,0 +1,86 @@ +package app.aaps.plugins.sync.garmin + +import app.aaps.shared.tests.TestBase +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.eq +import org.mockito.kotlin.isNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.timeout +import org.mockito.kotlin.verify +import java.net.Inet4Address +import java.net.Socket +import java.time.Duration + +class GarminSimulatorClientTest: TestBase() { + + private var device: GarminDevice? = null + private var app: GarminApplication? = null + private lateinit var client: GarminSimulatorClient + private val receiver: GarminReceiver = mock() { + on { onConnectDevice(any(), any(), any()) }.doAnswer { i -> + device = GarminDevice(client, i.getArgument(1), i.getArgument(2)) + app = GarminApplication( + client, device!!, client.iqApp.applicationID, client.iqApp.displayName) + } + } + + @BeforeEach + fun setup() { + client = GarminSimulatorClient(aapsLogger, receiver, 0) + } + + @Test + fun retrieveApplicationInfo() { + assertTrue(client.awaitReady(Duration.ofSeconds(10))) + val port = client.port + val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) + Socket(ip, port).use { socket -> + assertTrue(socket.isConnected) + verify(receiver).onConnect(client) + verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) + client.retrieveApplicationInfo(app!!.device, app!!.id, app!!.name!!) + } + verify(receiver).onApplicationInfo(app!!.device, app!!.id, true) + } + + @Test + fun receiveMessage() { + val payload = "foo".toByteArray() + assertTrue(client.awaitReady(Duration.ofSeconds(10))) + val port = client.port + val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) + Socket(ip, port).use { socket -> + assertTrue(socket.isConnected) + socket.getOutputStream().write(payload) + socket.getOutputStream().flush() + verify(receiver).onConnect(client) + verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) + } + verify(receiver).onReceiveMessage( + eq(client), eq(device!!.id), eq("SimApp"), + argThat { p -> payload.contentEquals(p) }) + } + + @Test + fun sendMessage() { + val payload = "foo".toByteArray() + assertTrue(client.awaitReady(Duration.ofSeconds(10))) + val port = client.port + val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) + Socket(ip, port).use { socket -> + assertTrue(socket.isConnected) + verify(receiver).onConnect(client) + verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) + assertNotNull(device) + assertNotNull(app) + client.sendMessage(app!!, payload) + } + verify(receiver).onSendMessage(eq(client), any(), eq(app!!.id), isNull()) + } +} \ No newline at end of file diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt index d89dfa9156..cfaa34b1d6 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/HttpServerTest.kt @@ -77,7 +77,7 @@ internal class HttpServerTest: TestBase() { HttpServer(aapsLogger, port).use { server -> server.registerEndpoint("/foo") { _: SocketAddress, uri: URI, _: String? -> assertEquals(URI("/foo"), uri) - "test" + HttpURLConnection.HTTP_OK to "test" } assertTrue(server.awaitReady(Duration.ofSeconds(10))) val resp = reqUri.toURL().openConnection() as HttpURLConnection From f3d1acffcd312431e1a915d55d7ddb460a186d1f Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Tue, 14 Nov 2023 16:42:38 +0100 Subject: [PATCH 04/70] Disable the garmin client for now. --- .../aaps/plugins/sync/garmin/GarminDeviceClient.kt | 9 +++++++-- .../app/aaps/plugins/sync/garmin/GarminMessenger.kt | 2 +- .../app/aaps/plugins/sync/garmin/GarminPlugin.kt | 11 +++++++---- .../aaps/plugins/sync/garmin/GarminSimulatorClient.kt | 5 +++-- .../plugins/sync/garmin/GarminSimulatorClientTest.kt | 2 +- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt index 358b7b9f16..8ef347d8a6 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt @@ -89,8 +89,13 @@ class GarminDeviceClient( bindLock.notifyAll() } if (notifyReceiver) receiver.onConnect(this@GarminDeviceClient) - ciq.connectedDevices?.forEach { d -> - receiver.onConnectDevice(this@GarminDeviceClient, d.deviceIdentifier, d.friendlyName) } + try { + ciq.connectedDevices?.forEach { d -> + receiver.onConnectDevice(this@GarminDeviceClient, d.deviceIdentifier, d.friendlyName) + } + } catch (e: Exception) { + aapsLogger.error(LTag.GARMIN, "getting devices failed", e) + } } override fun onServiceDisconnected(name: ComponentName?) { synchronized(bindLock) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt index 87c60ece7e..8f8671a6ec 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt @@ -26,7 +26,7 @@ class GarminMessenger( appIdNames.putAll(applicationIdNames) if (enableConnectIq) startDeviceClient() if (enableSimulator) { - appIdNames["SimAp"] = "SimulatorApp" + appIdNames["SimApp"] = "SimulatorApp" GarminSimulatorClient(aapsLogger, this) } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index 50f9410e7e..ce67a6e8bb 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -86,12 +86,13 @@ class GarminPlugin @Inject constructor( private val garminAapsKey get() = sp.getString("garmin_aaps_key", "") private fun onPreferenceChange(event: EventPreferenceChange) { - aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}") when (event.changedKey) { "communication_debug_mode" -> setupGarminMessenger() "communication_http", "communication_http_port" -> setupHttpServer() "garmin_aaps_key" -> sendPhoneAppMessage() + else -> return } + aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}") } private fun setupGarminMessenger() { @@ -120,7 +121,7 @@ class GarminPlugin @Inject constructor( .subscribe(::onNewBloodGlucose) ) setupHttpServer() - setupGarminMessenger() + // setupGarminMessenger() } fun setupHttpServer() { @@ -167,8 +168,10 @@ class GarminPlugin @Inject constructor( @VisibleForTesting fun onConnectDevice(device: GarminDevice) { - aapsLogger.info(LTag.GARMIN, "onConnectDevice $device sending glucose") - if (garminAapsKey.isNotEmpty()) sendPhoneAppMessage(device) + if (garminAapsKey.isNotEmpty()) { + aapsLogger.info(LTag.GARMIN, "onConnectDevice $device sending glucose") + sendPhoneAppMessage(device) + } } private fun sendPhoneAppMessage(device: GarminDevice) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt index f473773a2a..56b1ce1449 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt @@ -83,7 +83,8 @@ class GarminSimulatorClient( } } } catch (e: SocketException) { - aapsLogger.warn(LTag.GARMIN, "socket read failed ${e.message}") + aapsLogger.warn(LTag.GARMIN, "socket read failed ${e.message}") + break } } aapsLogger.info(LTag.GARMIN, "disconnect ${device.name}" ) @@ -162,7 +163,7 @@ class GarminSimulatorClient( override fun isDisposed() = serverSocket.isClosed override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) { - receiver.onApplicationInfo(device, appId, true) + receiver.onApplicationInfo(device, appId, appId == iqApp.applicationID) } private fun getConnection(device: GarminDevice): Connection? { diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt index a4b422616b..b713af0f05 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt @@ -62,7 +62,7 @@ class GarminSimulatorClientTest: TestBase() { verify(receiver).onConnect(client) verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) } - verify(receiver).onReceiveMessage( + verify(receiver, timeout(1_000)).onReceiveMessage( eq(client), eq(device!!.id), eq("SimApp"), argThat { p -> payload.contentEquals(p) }) } From 418229b84066b4af7816405104839714ac399a76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Nov 2023 08:34:28 +0000 Subject: [PATCH 05/70] chore(deps): bump com.android.tools.build:gradle from 8.1.3 to 8.1.4 Bumps com.android.tools.build:gradle from 8.1.3 to 8.1.4. --- updated-dependencies: - dependency-name: com.android.tools.build:gradle dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index ecca9a158d..04b3e46ec4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.1.3") + classpath("com.android.tools.build:gradle:8.1.4") classpath("com.google.gms:google-services:4.4.0") classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9") From ef3a8fbffa15f6fab63938b7a8054fc0a283102d Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 21 Nov 2023 13:48:54 +0100 Subject: [PATCH 06/70] show recalculated value in notification and sms --- .../persistentNotification/PersistentNotificationPlugin.kt | 2 +- .../main/general/smsCommunicator/SmsCommunicatorPlugin.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt index e9f46e8f65..23f27dddbb 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/persistentNotification/PersistentNotificationPlugin.kt @@ -124,7 +124,7 @@ class PersistentNotificationPlugin @Inject constructor( val lastBG = iobCobCalculator.ads.lastBg() val glucoseStatus = glucoseStatusProvider.glucoseStatusData if (lastBG != null) { - line1aa = profileUtil.fromMgdlToStringInUnits(lastBG.value) + line1aa = profileUtil.fromMgdlToStringInUnits(lastBG.recalculated) line1 = line1aa if (glucoseStatus != null) { line1 += (" Δ" + profileUtil.fromMgdlToSignedStringInUnits(glucoseStatus.delta) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/smsCommunicator/SmsCommunicatorPlugin.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/smsCommunicator/SmsCommunicatorPlugin.kt index 67bd789803..9726e1bd4a 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/smsCommunicator/SmsCommunicatorPlugin.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/general/smsCommunicator/SmsCommunicatorPlugin.kt @@ -356,11 +356,11 @@ class SmsCommunicatorPlugin @Inject constructor( var reply = "" val units = profileUtil.units if (actualBG != null) { - reply = rh.gs(R.string.sms_actual_bg) + " " + profileUtil.fromMgdlToStringInUnits(actualBG.value) + ", " + reply = rh.gs(R.string.sms_actual_bg) + " " + profileUtil.fromMgdlToStringInUnits(actualBG.recalculated) + ", " } else if (lastBG != null) { val agoMilliseconds = dateUtil.now() - lastBG.timestamp val agoMin = (agoMilliseconds / 60.0 / 1000.0).toInt() - reply = rh.gs(R.string.sms_last_bg) + " " + profileUtil.valueInCurrentUnitsDetect(lastBG.value) + " " + rh.gs(R.string.sms_min_ago, agoMin) + ", " + reply = rh.gs(R.string.sms_last_bg) + " " + profileUtil.valueInCurrentUnitsDetect(lastBG.recalculated) + " " + rh.gs(R.string.sms_min_ago, agoMin) + ", " } val glucoseStatus = glucoseStatusProvider.glucoseStatusData if (glucoseStatus != null) reply += rh.gs(R.string.sms_delta) + " " + profileUtil.fromMgdlToUnits(glucoseStatus.delta) + " " + units + ", " From 428d6c4feee05641132f3e55fa410fa9c79d7948 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 21 Nov 2023 14:18:24 +0100 Subject: [PATCH 07/70] CommandQueue: always create EffectiveProfileSwitch on success --- .../aaps/implementation/queue/CommandQueueImplementation.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index 9f41eacf24..cf12b20c70 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -125,7 +125,8 @@ class CommandQueueImplementation @Inject constructor( override fun run() { if (!result.success) { uiInteraction.runAlarm(result.comment, rh.gs(app.aaps.core.ui.R.string.failed_update_basal_profile), app.aaps.core.ui.R.raw.boluserror) - } else if (result.enacted || effective is ValueWrapper.Existing && effective.value.originalEnd < dateUtil.now() && effective.value.originalDuration != 0L) { + } else /* if (result.enacted || effective is ValueWrapper.Existing && effective.value.originalEnd < dateUtil.now() && effective.value.originalDuration != 0L) */ { + // Pump may return enacted == false if basal profile is the same, but IC/ISF can be different val nonCustomized = ProfileSealed.PS(it).convertToNonCustomizedProfile(dateUtil) EffectiveProfileSwitch( timestamp = dateUtil.now(), From 2feaa419a715000eff001e5d58ca12e073cca8f1 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 21 Nov 2023 14:28:12 +0100 Subject: [PATCH 08/70] Loop: Skip db change of ending previous TT --- .../src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt index fa6cd4cdd5..a27f79c4b6 100644 --- a/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt +++ b/plugins/aps/src/main/kotlin/app/aaps/plugins/aps/loop/LoopPlugin.kt @@ -79,6 +79,7 @@ import dagger.android.HasAndroidInjector import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import org.json.JSONObject +import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton import kotlin.math.abs @@ -135,6 +136,8 @@ class LoopPlugin @Inject constructor( disposable += rxBus .toObservable(EventTempTargetChange::class.java) .observeOn(aapsSchedulers.io) + // Skip db change of ending previous TT + .debounce(10L, TimeUnit.SECONDS) .subscribe({ invoke("EventTempTargetChange", true) }, fabricPrivacy::logException) } From 770ad1c4b8c68e9cb4a610a1d7ebd22a6c16a278 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 21 Nov 2023 17:11:48 +0100 Subject: [PATCH 09/70] remove unused code --- .../app/aaps/implementation/queue/CommandQueueImplementation.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt index cf12b20c70..1bf97596c8 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/queue/CommandQueueImplementation.kt @@ -119,7 +119,6 @@ class CommandQueueImplementation @Inject constructor( return@subscribe } aapsLogger.debug(LTag.PROFILE, "onEventProfileSwitchChanged") - val effective = repository.getEffectiveProfileSwitchActiveAt(dateUtil.now()).blockingGet() profileFunction.getRequestedProfile()?.let { setProfile(ProfileSealed.PS(it), it.interfaceIDs.nightscoutId != null, object : Callback() { override fun run() { From 6de44f967548843c61490d7c2c811c321e2ff3ec Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 22 Nov 2023 16:31:18 +0100 Subject: [PATCH 10/70] NSCv3: force http load on connect (sometimes onDisconnect is not called) --- .../aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt index 27a8bb7366..cf0404812a 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/nsclientV3/services/NSClientV3Service.kt @@ -167,7 +167,8 @@ class NSClientV3Service : DaggerService() { rxBus.send(EventNSClientNewLog("◄ WS", "Subscribed for: ${response.optString("collections")}")) // during disconnection updated data is not received // thus run non WS load to get missing data - nsClientV3Plugin.executeLoop("WS_CONNECT", forceNew = false) + nsClientV3Plugin.initialLoadFinished = false + nsClientV3Plugin.executeLoop("WS_CONNECT", forceNew = true) true } else { rxBus.send(EventNSClientNewLog("◄ WS", "Auth failed")) From 7088fcbabb987c9f16b5030fea29a2cef857a063 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 22 Nov 2023 16:44:15 +0100 Subject: [PATCH 11/70] New Crowdin updates (#3057) * New translations strings.xml (French) * New translations strings.xml (French) * New translations strings.xml (French) * New translations strings.xml (Russian) * New translations strings.xml (Slovak) * New translations strings.xml (Slovak) * New translations strings.xml (Russian) * New translations strings.xml (Russian) * New translations strings.xml (Russian) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Hebrew) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Russian) * New translations alert_titles.xml (Russian) * New translations strings.xml (Russian) * New translations strings.xml (Russian) --- .../src/main/res/values-ru-rRU/alert_titles.xml | 4 ++-- .../aps/src/main/res/values-fr-rFR/strings.xml | 2 ++ .../aps/src/main/res/values-iw-rIL/strings.xml | 2 ++ .../aps/src/main/res/values-ru-rRU/strings.xml | 6 +++--- .../aps/src/main/res/values-sk-rSK/strings.xml | 2 ++ .../src/main/res/values-ru-rRU/strings.xml | 2 +- .../src/main/res/values-nb-rNO/strings.xml | 2 +- .../main/src/main/res/values-nb-rNO/strings.xml | 2 +- .../sync/src/main/res/values-fr-rFR/strings.xml | 1 + .../sync/src/main/res/values-sk-rSK/strings.xml | 1 + .../combo/src/main/res/values-ru-rRU/strings.xml | 16 ++++++++-------- .../src/main/res/values-fr-rFR/strings.xml | 2 +- ui/src/main/res/values-ru-rRU/strings.xml | 2 +- wear/src/main/res/values-ru-rRU/strings.xml | 12 ++++++------ 14 files changed, 32 insertions(+), 24 deletions(-) diff --git a/insight/src/main/res/values-ru-rRU/alert_titles.xml b/insight/src/main/res/values-ru-rRU/alert_titles.xml index 4cb9822a47..9ae65db2bd 100644 --- a/insight/src/main/res/values-ru-rRU/alert_titles.xml +++ b/insight/src/main/res/values-ru-rRU/alert_titles.xml @@ -1,7 +1,7 @@ - Дать болюс - Недоставленный болюс + Ввести болюс + Несостоявшийся болюс Будильник Замените инфузионный набор TBR завершен diff --git a/plugins/aps/src/main/res/values-fr-rFR/strings.xml b/plugins/aps/src/main/res/values-fr-rFR/strings.xml index f4eebd4873..93cb120907 100644 --- a/plugins/aps/src/main/res/values-fr-rFR/strings.xml +++ b/plugins/aps/src/main/res/values-fr-rFR/strings.xml @@ -1,5 +1,7 @@ + Activer le ratio de sensibilité basé sur DTQ pour la modification du basal et la cible des glycémies + Utilise le dernier DTQ 24h/DTQ 7j pour calculer le ratio de sensibilité utilisé pour augmenter ou diminuer le taux de basal et ajuster aussi la cible de glycémie si ces options sont activées, de la même manière que Autosens. Il est recommandé de commencer avec cette option désactivée Facteur d\'ajustement Si dynamique % Facteur d\'ajustement pour Si dynamique. Définissez plus de 100 % pour des corrections plus agressives et moins de 100 % pour des corrections moins agressives. Cible temp. haute élève la sensibilité diff --git a/plugins/aps/src/main/res/values-iw-rIL/strings.xml b/plugins/aps/src/main/res/values-iw-rIL/strings.xml index 9e5a0e2956..f38db2d5d0 100644 --- a/plugins/aps/src/main/res/values-iw-rIL/strings.xml +++ b/plugins/aps/src/main/res/values-iw-rIL/strings.xml @@ -1,5 +1,7 @@ + אפשר יחס רגישות המבוסס על המינון היומי הכולל לשינוי מינון בזאלי וערכי מטרה + השתמש במינון יומי כולל של 24 שעות\\7 ימים האחרונים כדי לחשב יחס רגישות לצורך העלאה או הורדה של המינון הבזאלי ובנוסף להתאים את ערך המטרה אם אפשרויות אלו מופעלותת באופן דומה ל-Autosense. מומלץ להתחיל כשאפשרות זו כבויה כיוונון פקטור הרגישות הדינאמית % כיוונון פקטור הרגישות הדינאמית. הגדירו יותר מ-100% לקבלת תיקונים אגרסיביים יותר ופחות מ-100% לקבלת תיקונים עדינים יותר. ערך מטרה זמני גבוה מעלה את הרגישות diff --git a/plugins/aps/src/main/res/values-ru-rRU/strings.xml b/plugins/aps/src/main/res/values-ru-rRU/strings.xml index 9b405486f5..69ec53722a 100644 --- a/plugins/aps/src/main/res/values-ru-rRU/strings.xml +++ b/plugins/aps/src/main/res/values-ru-rRU/strings.xml @@ -10,9 +10,9 @@ = 100]]> OpenAPS СМБ Динамический ISF - Как часто СМБ будут подаваться в минутах - Сопротивляемость понижает цель - При обнаружении сопротивляемости целевой уровень гликемии понижается + Как часто SMB будут подаваться в минутах + Резистентность понижает цель + При обнаружении резистентности целевой уровень гликемии понижается Чувствительность поднимает цель При обнаружении чувствительности целевой уровень глюкозы повышается модуль не активен diff --git a/plugins/aps/src/main/res/values-sk-rSK/strings.xml b/plugins/aps/src/main/res/values-sk-rSK/strings.xml index 606aceb1d2..035f543be6 100644 --- a/plugins/aps/src/main/res/values-sk-rSK/strings.xml +++ b/plugins/aps/src/main/res/values-sk-rSK/strings.xml @@ -1,5 +1,7 @@ + Povoliť, aby citlivosť založená na CDD mohla upravovať bazál a cieľovú glykémiu + K výpočtu citlivosti použitej pre zvýšenie, alebo zníženie bazálu sa používa CDD za posledných 24 h / 7 D a tiež upravuje cieľovú glykémiu, pokiaľ je táto možnosť povolená, rovnako, ako to robí Autosens. Túto možnosť je doporučené na začiatok vypnúť. Korekčný faktor pre Dynamickú ISF v % Korekčný faktor pre dynamickú ISF. Pre agresivnejšie korekčné dávky nastavte hodnoty väčšie ako 100%, pre menej agresívne korekcie, menšie než 100%. Vysoký dočasný cieľ zvýši citlivosť diff --git a/plugins/configuration/src/main/res/values-ru-rRU/strings.xml b/plugins/configuration/src/main/res/values-ru-rRU/strings.xml index e27f2817c2..4421a56416 100644 --- a/plugins/configuration/src/main/res/values-ru-rRU/strings.xml +++ b/plugins/configuration/src/main/res/values-ru-rRU/strings.xml @@ -35,7 +35,7 @@ Напоминание: новые профили инсулина требуют как минимум 5 часов длительности действия DIA. 5-6 часовая DIA эквивалентна трехчасовой на старых профилях. Профиль Переключить профиль - Пропустить Мастер установки + Пропустить Мастер настройки Нажмите на кнопку ниже, чтобы AAPS мог предложить/внести изменения в базал Плагин чувствительности Sensitivity применяется для определения чувствительности к инсулину и вычисления активных углеводов COB. Дополнительная информация: https://wiki.aaps.app/en/latest/Configuration/Sensitivity-detection-and-COB.html diff --git a/plugins/constraints/src/main/res/values-nb-rNO/strings.xml b/plugins/constraints/src/main/res/values-nb-rNO/strings.xml index 7e56ad8011..510add70ed 100644 --- a/plugins/constraints/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/constraints/src/main/res/values-nb-rNO/strings.xml @@ -16,7 +16,7 @@ Versjon %1$s utløper den %2$s - Bruker rekalkulerte data + Rekalkulerte data benyttes fordi noe BS-data mangler eller at BS ankommer på et ikke-forventet tidspunkt. Vil nullstille seg etter ca 24 timer. Ingen handling kreves BS for nær:\n%1$s\n%2$s beregnet på nytt doble registreringer diff --git a/plugins/main/src/main/res/values-nb-rNO/strings.xml b/plugins/main/src/main/res/values-nb-rNO/strings.xml index f12a2a69a2..730cb98170 100644 --- a/plugins/main/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/main/src/main/res/values-nb-rNO/strings.xml @@ -211,7 +211,7 @@ Endre dine inndata! OpenAPS Opplaster-batteri - BS data status + Status BS-data Innstillinger for hurtigknapp Hold skjermen påslått Forhindre Android fra å slå av skjermen. Mobilen vil bruke mye batteri hvis den ikke kobles til strømledning. diff --git a/plugins/sync/src/main/res/values-fr-rFR/strings.xml b/plugins/sync/src/main/res/values-fr-rFR/strings.xml index 2af445a728..c5fbc87a84 100644 --- a/plugins/sync/src/main/res/values-fr-rFR/strings.xml +++ b/plugins/sync/src/main/res/values-fr-rFR/strings.xml @@ -17,6 +17,7 @@ Synchronise vos données avec Nightscout en utilisant l\'API v3 Bloqué par les options de recharge Bloqué par les options de connectivité + Aucune connectivité Version de Nightscout non supportée OAPS UPLD diff --git a/plugins/sync/src/main/res/values-sk-rSK/strings.xml b/plugins/sync/src/main/res/values-sk-rSK/strings.xml index b4a49e688e..83b5b878f1 100644 --- a/plugins/sync/src/main/res/values-sk-rSK/strings.xml +++ b/plugins/sync/src/main/res/values-sk-rSK/strings.xml @@ -17,6 +17,7 @@ Synchronizuje vaše dáta s Nightscoutom pomocou v3 API Zablokované možnosti nabíjania Zablokované možnosti pripojenia + Žiadne pripojenie Nepodporovaná verzia Nighscoutu OAPS UPLD diff --git a/pump/combo/src/main/res/values-ru-rRU/strings.xml b/pump/combo/src/main/res/values-ru-rRU/strings.xml index a0e9709c2a..77f6a643c0 100644 --- a/pump/combo/src/main/res/values-ru-rRU/strings.xml +++ b/pump/combo/src/main/res/values-ru-rRU/strings.xml @@ -28,23 +28,23 @@ Нормальный Необходимо обновить часы помпы Предупреждение об отмене скорости временного базала подтверждено - Не удалось подключиться к помпе. Болюс не подан - Подача болюса не состоялась. Чтобы удостовериться, проверьте помпу во избежание двойного болюса и повторите подачу. Для защиты от ложных срабатываний болюсы не повторяются автоматически. - Подано только %1$.2f ед. из запрошенного болюса %2$.2f ед. из-за ошибки. Пожалуйста, проверьте помпу, чтобы удостовериться в этом и принять соответствующие меры. - Подача болюса и проверка истории помпы не состоялись, пожалуйста проверьте помпу. Если болюс был подан, он будет добавлен в назначения во время следующего соединения с помпой. + Не удалось подключиться к помпе. Болюс не введен + Болюс не введен. Чтобы удостовериться, проверьте помпу во избежание двойного болюса и повторите подачу. Для защиты от ложных срабатываний болюсы не повторяются автоматически. + Введено только %1$.2f ед. из запрошенного болюса %2$.2f ед. из-за ошибки. Пожалуйста, проверьте помпу, чтобы удостовериться в этом и принять соответствующие меры. + Ввод болюса и проверка истории помпы не состоялись, пожалуйста проверьте помпу. Если болюс был введен, он будет добавлен в назначения во время следующего соединения с помпой. В резервуаре недостаточно инсулина для болюса Недопустимые установки помпы, проверьте документацию и убедитесь, что меню Quick Info называется QUICK INFO, используя приложение 360 для конфигурации помпы. Чтение базального профиля - История событий помпы изменилась с момента вычисления болюса. Болюс не подан. Пожалуйста пересчитайте потребность в болюсе. - Болюс подан успешно, но запись о назначении не сделана. Это может быть вызвано тем, что за последние две минуты назначено два болюса одного объема. Пожалуйста проверьте историю событий помпы и записи о назначениях и добавьте пропущенные записи из Портала назначений. Не добавляйте записи с одним и тем же временем и одинаковым объемом. + История событий помпы изменилась с момента вычисления болюса. Болюс не введен. Пожалуйста пересчитайте потребность в болюсе. + Болюс введен успешно, но запись о назначении не сделана. Это может быть вызвано тем, что за последние две минуты назначено два болюса одного объема. Пожалуйста проверьте историю событий помпы и записи о назначениях и добавьте пропущенные записи из Портала назначений. Не добавляйте записи с одним и тем же временем и одинаковым объемом. Временная верхняя цель отклонена т. к. калькуляция не учитывала недавние изменения в истории событий помпы Обновление статуса помпы Скорость базала на помпе изменилась и вскоре будет обновлена Скорость базала на помпе изменилась, но учесть ее не удалось Проверка изменений в истории событий помпы Только что импортировано несколько болюсов с одинаковым количеством инсулина в течение одной минуты. В лог лечения может быть добавлена только одна запись. Пожалуйста проверьте помпу и вручную введите запись о болюсе через вкладку портала назначений. Убедитесь, что данному времени соответствует только одна запись о болюсе. - Новый болюс старше 24 часов или запись относится к будущему. Пожалуйста убедитесь что дата на помпе установлена правильно. - Время/дата поданного болюса неверны. Вероятно, кол-во активного инсулина IOB также неверно. Проверьте время/дату помпы. + Новый болюс старше 24 часов или запись относится к будущему. Убедитесь что дата на помпе установлена правильно. + Время/дата болюса неверны. Вероятно, кол-во активного инсулина IOB также неверно. Проверьте время/дату помпы. Отсчет болюса Отсчет временного базала TBR Болюс остановлен diff --git a/pump/omnipod-common/src/main/res/values-fr-rFR/strings.xml b/pump/omnipod-common/src/main/res/values-fr-rFR/strings.xml index 5582068512..de3cc9fd83 100644 --- a/pump/omnipod-common/src/main/res/values-fr-rFR/strings.xml +++ b/pump/omnipod-common/src/main/res/values-fr-rFR/strings.xml @@ -53,7 +53,7 @@ ID unique Numéro de lot Numéro de série - Pod Expiré + Pod expire le Dernière connexion Dernier bolus Débit de Basal Temp. diff --git a/ui/src/main/res/values-ru-rRU/strings.xml b/ui/src/main/res/values-ru-rRU/strings.xml index 00cb9201f0..f55731100c 100644 --- a/ui/src/main/res/values-ru-rRU/strings.xml +++ b/ui/src/main/res/values-ru-rRU/strings.xml @@ -42,7 +42,7 @@ Внести запись о смене места катетера помпы Внести запись о замене картриджа инсулина - Не подавать болюс, только внести запись + Не вводить болюс, только внести запись Повторно использовать %1$d%%%2$dч Сдвиг по времени diff --git a/wear/src/main/res/values-ru-rRU/strings.xml b/wear/src/main/res/values-ru-rRU/strings.xml index e91a4e0d36..c679e5131d 100644 --- a/wear/src/main/res/values-ru-rRU/strings.xml +++ b/wear/src/main/res/values-ru-rRU/strings.xml @@ -116,7 +116,7 @@ ПОДТВЕРДИТЬ сдвиг по времени Болюс - Подается болюс + Вводится болюс нажмите для отмены ОТМЕНИТЬ БОЛЮС Помпа @@ -161,10 +161,10 @@ Ваш цвет: Ваша цветовая насыщенность: Непрозрачность вашего цвета: - Подается болюс - Беззвучная подача болюса - Подача и отмена болюса - Подача и отмена болюса с меньшими вибрациями + Вводится болюс + Беззвучный ввод болюса + Ввод и отмена болюса + Введение и отмена болюса с меньшими вибрациями Выкл Во время зарядки Режим: всегда вкл @@ -192,7 +192,7 @@ 000г 00,0 0,00 ед - Оповещение о переходе AAPS в режим энергосбережения + Отложить оповещения AAPS Отправка команды отложить оповещения AAPS ч н From a36081d3dfa53b0b4011be45aef8e2cb2a13a1c0 Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Sun, 19 Nov 2023 23:10:20 +0100 Subject: [PATCH 12/70] Fix Garmin communication. Use Garmin lib rather than aidl, get rid of not needed functionality. --- buildSrc/src/main/kotlin/Libs.kt | 5 +- .../test-module-dependencies.gradle.kts | 3 + plugins/sync/build.gradle.kts | 8 +- .../sync/garmin/GarminDeviceClientTest.kt | 301 ++++++++++++++++++ .../connectiq/IConnectIQService.aidl | 30 -- .../com/garmin/android/connectiq/IQApp.aidl | 12 - .../garmin/android/connectiq/IQDevice.aidl | 11 - .../garmin/android/connectiq/IQMessage.aidl | 20 -- .../plugins/sync/garmin/GarminApplication.kt | 13 +- .../aaps/plugins/sync/garmin/GarminClient.kt | 6 +- .../aaps/plugins/sync/garmin/GarminDevice.kt | 24 +- .../plugins/sync/garmin/GarminDeviceClient.kt | 125 ++++---- .../plugins/sync/garmin/GarminMessenger.kt | 66 +--- .../aaps/plugins/sync/garmin/GarminPlugin.kt | 19 +- .../plugins/sync/garmin/GarminReceiver.kt | 13 - .../sync/garmin/GarminSimulatorClient.kt | 22 +- .../sync/garmin/GarminMessengerTest.kt | 138 +++----- .../sync/garmin/GarminSimulatorClientTest.kt | 49 +-- 18 files changed, 484 insertions(+), 381 deletions(-) create mode 100644 plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt delete mode 100644 plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl delete mode 100644 plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl delete mode 100644 plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl delete mode 100644 plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 719175d1a0..db6e48f53a 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -152,8 +152,11 @@ object Libs { } object Mockito { + private const val mockitoVersion = "5.6.0" - const val jupiter = "org.mockito:mockito-junit-jupiter:5.6.0" + const val android = "org.mockito:mockito-android:$mockitoVersion" + const val core = "org.mockito:mockito-core:$mockitoVersion" + const val jupiter = "org.mockito:mockito-junit-jupiter:$mockitoVersion" const val kotlin = "org.mockito.kotlin:mockito-kotlin:5.1.0" } diff --git a/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts b/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts index 22fc2175b7..bd520b0863 100644 --- a/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts +++ b/buildSrc/src/main/kotlin/test-module-dependencies.gradle.kts @@ -22,6 +22,9 @@ dependencies { androidTestImplementation(Libs.AndroidX.Test.rules) androidTestImplementation(Libs.Google.truth) androidTestImplementation(Libs.AndroidX.Test.uiAutomator) + androidTestImplementation(Libs.Mockito.core) + androidTestImplementation(Libs.Mockito.android) + androidTestImplementation(Libs.Mockito.kotlin) } tasks.withType { diff --git a/plugins/sync/build.gradle.kts b/plugins/sync/build.gradle.kts index 1bea79a6c3..81f4671523 100644 --- a/plugins/sync/build.gradle.kts +++ b/plugins/sync/build.gradle.kts @@ -9,9 +9,6 @@ plugins { android { namespace = "app.aaps.plugins.sync" - buildFeatures { - aidl = true - } } dependencies { @@ -33,6 +30,7 @@ dependencies { testImplementation(project(":shared:tests")) testImplementation(project(":implementation")) testImplementation(project(":plugins:aps")) + androidTestImplementation(project(":shared:tests")) // OpenHuman api(Libs.Squareup.Okhttp3.okhttp) @@ -52,6 +50,10 @@ dependencies { // DataLayerListenerService api(Libs.Google.Android.PlayServices.wearable) + // Garmin + api("com.garmin.connectiq:ciq-companion-app-sdk:2.0.2@aar") + androidTestImplementation("com.garmin.connectiq:ciq-companion-app-sdk:2.0.2@aar") + kapt(Libs.Dagger.compiler) kapt(Libs.Dagger.androidProcessor) } \ No newline at end of file diff --git a/plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt b/plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt new file mode 100644 index 0000000000..4f63ea3671 --- /dev/null +++ b/plugins/sync/src/androidTest/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClientTest.kt @@ -0,0 +1,301 @@ +package app.aaps.plugins.sync.garmin + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.ServiceConnection +import android.os.Binder +import android.os.IBinder +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.aaps.shared.tests.TestBase +import com.garmin.android.apps.connectmobile.connectiq.IConnectIQService +import com.garmin.android.connectiq.ConnectIQ +import com.garmin.android.connectiq.IQApp +import com.garmin.android.connectiq.IQDevice +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.timeout +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.kotlin.argThat +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import java.util.concurrent.Executor + +@RunWith(AndroidJUnit4::class) +class GarminDeviceClientTest: TestBase() { + private val serviceDescriptor = "com.garmin.android.apps.connectmobile.connectiq.IConnectIQService" + private lateinit var client: GarminDeviceClient + private lateinit var serviceConnection: ServiceConnection + private lateinit var device: GarminDevice + private val packageName = "TestPackage" + private val actions = mutableMapOf() + // Maps app ids to intent actions. + private val receivers = mutableMapOf() + + private val receiver = mock() + private val binder = mock() { + on { isBinderAlive } doReturn true + } + private val ciqService = mock() { + on { asBinder() } doReturn binder + on { connectedDevices } doReturn listOf(IQDevice(1L, "TDevice")) + on { registerApp(any(), any(), any()) }.doAnswer { i -> + receivers[i.getArgument(0).applicationId] = i.getArgument(1) + } + } + private val context = mock() { + on { packageName } doReturn this@GarminDeviceClientTest.packageName + on { registerReceiver(any(), any()) } doAnswer { i -> + actions[i.getArgument(1).getAction(0)] = i.getArgument(0) + Intent() + } + on { unregisterReceiver(any()) } doAnswer { i -> + val keys = actions.entries.filter {(_, br) -> br == i.getArgument(0) }.map { (k, _) -> k } + keys.forEach { k -> actions.remove(k) } + } + on { bindService(any(), eq(Context.BIND_AUTO_CREATE), any(), any()) }. doAnswer { i -> + serviceConnection = i.getArgument(3) + i.getArgument(2).execute { + serviceConnection.onServiceConnected( + GarminDeviceClient.CONNECTIQ_SERVICE_COMPONENT, + Binder().apply { attachInterface(ciqService, serviceDescriptor) }) + } + true + } + on { bindService(any(), any(), eq(Context.BIND_AUTO_CREATE)) }. doAnswer { i -> + serviceConnection = i.getArgument(1) + serviceConnection.onServiceConnected( + GarminDeviceClient.CONNECTIQ_SERVICE_COMPONENT, + Binder().apply { attachInterface(ciqService, serviceDescriptor) }) + true + } + } + + @Before + fun setup() { + client = GarminDeviceClient(aapsLogger, context, receiver, retryWaitFactor = 0L) + device = GarminDevice(client, 1L, "TDevice") + verify(receiver, timeout(2_000L)).onConnect(client) + } + + @After + fun shutdown() { + if (::client.isInitialized) client.dispose() + assertEquals(0, actions.size) // make sure all broadcastReceivers were unregistered + verify(context).unbindService(serviceConnection) + } + + @Test + fun connect() { + } + + @Test + fun disconnect() { + serviceConnection.onServiceDisconnected(GarminDeviceClient.CONNECTIQ_SERVICE_COMPONENT) + verify(receiver).onDisconnect(client) + assertEquals(0, actions.size) + } + + @Test + fun connectedDevices() { + assertEquals(listOf(device), client.connectedDevices) + verify(ciqService).connectedDevices + } + + @Test + fun reconnectDeadBinder() { + whenever(binder.isBinderAlive).thenReturn(false, true) + assertEquals(listOf(device), client.connectedDevices) + + verify(ciqService).connectedDevices + verify(ciqService, times(2)).asBinder() + verify(context, times(2)) + .bindService(any(), eq(Context.BIND_AUTO_CREATE), any(), any()) + + verifyNoMoreInteractions(ciqService) + verifyNoMoreInteractions(receiver) + } + + @Test + fun sendMessage() { + val appId = "APPID1" + val data = "Hello, World!".toByteArray() + + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data) + verify(ciqService).sendMessage( + argThat { iqMsg -> data.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat { iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId) + } + actions[client.sendMessageAction]!!.onReceive(context, intent) + actions[client.sendMessageAction]!!.onReceive(context, intent) // extra on receive will be ignored + verify(receiver).onSendMessage(client, device.id, appId, null) + } + + @Test + fun sendMessage_failNoRetry() { + val appId = "APPID1" + val data = "Hello, World!".toByteArray() + + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data) + verify(ciqService).sendMessage( + argThat { iqMsg -> data.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.FAILURE_MESSAGE_TOO_LARGE.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId) + } + actions[client.sendMessageAction]!!.onReceive(context, intent) + verify(receiver).onSendMessage(client, device.id, appId, "error FAILURE_MESSAGE_TOO_LARGE") + } + + @Test + fun sendMessage_failRetry() { + val appId = "APPID1" + val data = "Hello, World!".toByteArray() + + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data) + verify(ciqService).sendMessage( + argThat { iqMsg -> data.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.FAILURE_DURING_TRANSFER.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId) + } + actions[client.sendMessageAction]!!.onReceive(context, intent) + verifyNoMoreInteractions(receiver) + + // Verify retry ... + verify(ciqService, timeout(10_000L).times( 2)).sendMessage( + argThat { iqMsg -> data.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + intent.putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + actions[client.sendMessageAction]!!.onReceive(context, intent) + verify(receiver).onSendMessage(client, device.id, appId, null) + } + + @Test + fun sendMessage_2toSameApp() { + val appId = "APPID1" + val data1 = "m1".toByteArray() + val data2 = "m2".toByteArray() + + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data1) + client.sendMessage(GarminApplication(device, appId, "$appId-name"), data2) + verify(ciqService).sendMessage( + argThat { iqMsg -> data1.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + verify(ciqService, atLeastOnce()).asBinder() + verifyNoMoreInteractions(ciqService) + + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId) + } + actions[client.sendMessageAction]!!.onReceive(context, intent) + verify(receiver).onSendMessage(client, device.id, appId, null) + + verify(ciqService, timeout(5000L)).sendMessage( + argThat { iqMsg -> data2.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId }) + + actions[client.sendMessageAction]!!.onReceive(context, intent) + verify(receiver, times(2)).onSendMessage(client, device.id, appId, null) + } + + @Test + fun sendMessage_2to2Apps() { + val appId1 = "APPID1" + val appId2 = "APPID2" + val data1 = "m1".toByteArray() + val data2 = "m2".toByteArray() + + client.sendMessage(GarminApplication(device, appId1, "$appId1-name"), data1) + client.sendMessage(GarminApplication(device, appId2, "$appId2-name"), data2) + verify(ciqService).sendMessage( + argThat { iqMsg -> data1.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId1 }) + verify(ciqService, timeout(5000L)).sendMessage( + argThat { iqMsg -> data2.contentEquals(iqMsg.messageData) + && iqMsg.notificationPackage == packageName + && iqMsg.notificationAction == client.sendMessageAction }, + argThat {iqDevice -> iqDevice.deviceIdentifier == device.id }, + argThat { iqApp -> iqApp?.applicationId == appId2 }) + + val intent1 = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId1) + } + actions[client.sendMessageAction]!!.onReceive(context, intent1) + verify(receiver).onSendMessage(client, device.id, appId1, null) + + val intent2 = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_STATUS, ConnectIQ.IQMessageStatus.SUCCESS.ordinal) + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_APPLICATION_ID, appId2) + } + actions[client.sendMessageAction]!!.onReceive(context, intent2) + verify(receiver).onSendMessage(client, device.id, appId2, null) + } + + @Test + fun receiveMessage() { + val app = GarminApplication(GarminDevice(client, 1L, "D1"), "APPID1", "N1") + client.registerForMessages(app) + assertTrue(receivers.contains(app.id)) + val intent = Intent().apply { + putExtra(GarminDeviceClient.EXTRA_REMOTE_DEVICE, app.device.toIQDevice()) + putExtra(GarminDeviceClient.EXTRA_PAYLOAD, "foo".toByteArray()) + } + actions[receivers[app.id]]!!.onReceive(context, intent) + verify(receiver).onReceiveMessage( + eq(client), + eq(app.device.id), + eq(app.id), + argThat { payload -> "foo" == String(payload) }) + } +} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl b/plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl deleted file mode 100644 index 8aaf0eb761..0000000000 --- a/plugins/sync/src/main/aidl/com/garmin/android/apps/connectmobile/connectiq/IConnectIQService.aidl +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (C) 2014 Garmin International Ltd. - * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. - */ -//IConnectIQService -package com.garmin.android.apps.connectmobile.connectiq; - -import com.garmin.android.connectiq.IQDevice; -import com.garmin.android.connectiq.IQApp; -import com.garmin.android.connectiq.IQMessage; - -interface IConnectIQService { - boolean openStore(String applicationID); - List getConnectedDevices(); - List getKnownDevices(); - - // Remote device methods - int getStatus(in IQDevice device); - - // Messages and Commands - oneway void getApplicationInfo(String notificationPackage, String notificationAction, in IQDevice device, String applicationID); - oneway void openApplication(String notificationPackage, String notificationAction, in IQDevice device, in IQApp app); - - // Pending intent will be fired to let the sdk know a message has been transferred. - oneway void sendMessage(in IQMessage message, in IQDevice device, in IQApp app); - oneway void sendImage(in IQMessage image, in IQDevice device, in IQApp app); - - // registers a companion app with the remote service so that it can receive messages from remote device. - oneway void registerApp(in IQApp app, String notificationAction, String notificationPackage); -} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl deleted file mode 100644 index 2115dafd35..0000000000 --- a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQApp.aidl +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (C) 2014 Garmin International Ltd. - * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. - */ -package com.garmin.android.connectiq; - -parcelable IQApp { - String applicationID; - int status; - String displayName; - int version; -} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl deleted file mode 100644 index ec1d4f3217..0000000000 --- a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQDevice.aidl +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (C) 2014 Garmin International Ltd. - * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. - */ -package com.garmin.android.connectiq; - -parcelable IQDevice { - long deviceIdentifier; - String friendlyName; - int status; -} \ No newline at end of file diff --git a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl b/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl deleted file mode 100644 index cbf6335fb0..0000000000 --- a/plugins/sync/src/main/aidl/com/garmin/android/connectiq/IQMessage.aidl +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (C) 2014 Garmin International Ltd. - * Subject to Garmin SDK License Agreement and Wearables Application Developer Agreement. - */ -package com.garmin.android.connectiq; - -parcelable IQMessage { - const int SUCCESS = 0; - const int FAILURE_UNKNOWN = 1; - const int FAILURE_INVALID_FORMAT = 2; - const int FAILURE_MESSAGE_TOO_LARGE = 3; - const int FAILURE_UNSUPPORTED_TYPE = 4; - const int FAILURE_DURING_TRANSFER = 5; - const int FAILURE_INVALID_DEVICE = 6; - const int FAILURE_DEVICE_NOT_CONNECTED = 7; - - byte[] messageData; - String notificationPackage; - String notificationAction; -} \ No newline at end of file diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt index 3aec9257a9..6ff37f0a62 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminApplication.kt @@ -1,20 +1,11 @@ package app.aaps.plugins.sync.garmin data class GarminApplication( - val client: GarminClient, val device: GarminDevice, val id: String, val name: String?) { - enum class Status { - @Suppress("UNUSED") - UNKNOWN, - INSTALLED, - @Suppress("UNUSED") - NOT_INSTALLED, - @Suppress("UNUSED") - NOT_SUPPORTED; - } + val client get() = device.client override fun equals(other: Any?): Boolean { if (this === other) return true @@ -35,5 +26,7 @@ data class GarminApplication( result = 31 * result + id.hashCode() return result } + + override fun toString() = "A[$device:$id:$name]" } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt index 9c1631497d..6333f2ae82 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminClient.kt @@ -6,8 +6,10 @@ interface GarminClient: Disposable { /** Name of the client. */ val name: String - /** Asynchronously retrieves status information for the given application. */ - fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) + val connectedDevices: List + + /** Register to receive messages from the given up. */ + fun registerForMessages(app: GarminApplication) /** Asynchronously sends a message to an application. */ fun sendMessage(app: GarminApplication, data: ByteArray) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt index b0fc0ead8f..1df255d38a 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDevice.kt @@ -5,34 +5,16 @@ import com.garmin.android.connectiq.IQDevice data class GarminDevice( val client: GarminClient, val id: Long, - var name: String, - var status: Status = Status.UNKNOWN) { + var name: String) { constructor(client: GarminClient, iqDevice: IQDevice): this( client, iqDevice.deviceIdentifier, - iqDevice.friendlyName, - Status.from(iqDevice.status)) {} - - enum class Status { - NOT_PAIRED, - NOT_CONNECTED, - CONNECTED, - UNKNOWN; - - companion object { - fun from(ordinal: Int?): Status = - values().firstOrNull { s -> s.ordinal == ordinal } ?: UNKNOWN - } - } - + iqDevice.friendlyName) {} override fun toString(): String = "D[$name/$id]" - fun toIQDevice() = IQDevice().apply { - deviceIdentifier = id - friendlyName = name - status = Status.UNKNOWN.ordinal } + fun toIQDevice() = IQDevice(id, name) override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt index 8ef347d8a6..ec81ddb512 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminDeviceClient.kt @@ -6,22 +6,24 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.ServiceConnection +import android.os.Build import android.os.IBinder import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.utils.waitMillis import com.garmin.android.apps.connectmobile.connectiq.IConnectIQService +import com.garmin.android.connectiq.ConnectIQ.IQMessageStatus import com.garmin.android.connectiq.IQApp import com.garmin.android.connectiq.IQDevice import com.garmin.android.connectiq.IQMessage import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers import org.jetbrains.annotations.VisibleForTesting -import java.time.Duration +import java.lang.Thread.UncaughtExceptionHandler import java.time.Instant import java.util.LinkedList -import java.util.Locale import java.util.Queue +import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -33,26 +35,31 @@ class GarminDeviceClient( private val retryWaitFactor: Long = 5L): Disposable, GarminClient { override val name = "Device" + private var executor = Executors.newSingleThreadExecutor { r -> + Thread(r).apply { + name = "Garmin callback" + isDaemon = true + uncaughtExceptionHandler = UncaughtExceptionHandler { _, e -> + aapsLogger.error(LTag.GARMIN, "ConnectIQ callback failed", e) } + } + } private var bindLock = Object() private var ciqService: IConnectIQService? = null get() { - val waitUntil = Instant.now().plusSeconds(2) synchronized (bindLock) { - while(field?.asBinder()?.isBinderAlive != true) { + if (field?.asBinder()?.isBinderAlive != true) { field = null if (state !in arrayOf(State.BINDING, State.RECONNECTING)) { aapsLogger.info(LTag.GARMIN, "reconnecting to ConnectIQ service") state = State.RECONNECTING - context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE) + bindService() } - // Wait for the connection, that is the call to onServiceConnected. - val wait = Duration.between(Instant.now(), waitUntil) - if (wait > Duration.ZERO) bindLock.waitMillis(wait.toMillis()) - if (field == null) { + bindLock.waitMillis(2_000L) + if (field?.asBinder()?.isBinderAlive != true) { + field = null // The [serviceConnection] didn't have a chance to reassign ciqService, // i.e. the wait timed out. Give up. aapsLogger.warn(LTag.GARMIN, "no ciqservice $this") - return null } } return field @@ -80,7 +87,7 @@ class GarminDeviceClient( override fun onServiceConnected(name: ComponentName?, service: IBinder?) { var notifyReceiver: Boolean val ciq: IConnectIQService - synchronized (bindLock) { + synchronized(bindLock) { aapsLogger.info(LTag.GARMIN, "ConnectIQ App connected") ciq = IConnectIQService.Stub.asInterface(service) notifyReceiver = state != State.RECONNECTING @@ -89,14 +96,8 @@ class GarminDeviceClient( bindLock.notifyAll() } if (notifyReceiver) receiver.onConnect(this@GarminDeviceClient) - try { - ciq.connectedDevices?.forEach { d -> - receiver.onConnectDevice(this@GarminDeviceClient, d.deviceIdentifier, d.friendlyName) - } - } catch (e: Exception) { - aapsLogger.error(LTag.GARMIN, "getting devices failed", e) - } } + override fun onServiceDisconnected(name: ComponentName?) { synchronized(bindLock) { aapsLogger.info(LTag.GARMIN, "ConnectIQ App disconnected") @@ -114,9 +115,21 @@ class GarminDeviceClient( aapsLogger.info(LTag.GARMIN, "binding to ConnectIQ service") registerReceiver(sendMessageAction, ::onSendMessage) state = State.BINDING - context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE) + bindService() } + private fun bindService() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + context.bindService(serviceIntent, Context.BIND_AUTO_CREATE, executor, ciqServiceConnection) + } else { + context.bindService(serviceIntent, ciqServiceConnection, Context.BIND_AUTO_CREATE) + } + } + + override val connectedDevices: List + get() = ciqService?.connectedDevices?.map { iqDevice -> GarminDevice(this, iqDevice) } + ?: emptyList() + override fun isDisposed() = state == State.DISPOSED override fun dispose() { broadcastReceiver.forEach { context.unregisterReceiver(it) } @@ -143,35 +156,14 @@ class GarminDeviceClient( context.registerReceiver(recv, IntentFilter(action)) } - override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) { - val action = createAction("APPLICATION_INFO_${device.id}_$appId") + override fun registerForMessages(app: GarminApplication) { + aapsLogger.info(LTag.GARMIN, "registerForMessage $name $app") + val action = createAction("ON_MESSAGE_${app.device.id}_${app.id}") + val iqApp = IQApp(app.id) synchronized (registeredActions) { if (!registeredActions.contains(action)) { - registerReceiver(action) { intent -> onApplicationInfo(appId, device, intent) } - } - registeredActions.add(action) - } - ciqService?.getApplicationInfo(context.packageName, action, device.toIQDevice(), appId) - } - - /** Receives application info callbacks from ConnectIQ app.*/ - private fun onApplicationInfo(appId: String, device: GarminDevice, intent: Intent) { - val receivedAppId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.lowercase(Locale.getDefault()) - val version = intent.getIntExtra(EXTRA_APPLICATION_VERSION, -1) - val isInstalled = receivedAppId != null && version >= 0 && version != 65535 - - if (isInstalled) registerForMessages(device.id, appId) - receiver.onApplicationInfo(device, appId, isInstalled) - } - - private fun registerForMessages(deviceId: Long, appId: String) { - aapsLogger.info(LTag.GARMIN, "registerForMessage $name $appId") - val action = createAction("ON_MESSAGE_${deviceId}_$appId") - val app = IQApp().apply { applicationID = appId; displayName = "" } - synchronized (registeredActions) { - if (!registeredActions.contains(action)) { - registerReceiver(action) { intent: Intent -> onReceiveMessage(app, intent) } - ciqService?.registerApp(app, action, context.packageName) + registerReceiver(action) { intent: Intent -> onReceiveMessage(iqApp, intent) } + ciqService?.registerApp(iqApp, action, context.packageName) registeredActions.add(action) } else { aapsLogger.info(LTag.GARMIN, "registerForMessage $action already registered") @@ -184,14 +176,15 @@ class GarminDeviceClient( val iqDevice = intent.getParcelableExtra(EXTRA_REMOTE_DEVICE) as IQDevice? val data = intent.getByteArrayExtra(EXTRA_PAYLOAD) if (iqDevice != null && data != null) - receiver.onReceiveMessage(this, iqDevice.deviceIdentifier, iqApp.applicationID, data) + receiver.onReceiveMessage(this, iqDevice.deviceIdentifier, iqApp.applicationId, data) } /** Receives callback from ConnectIQ about message transfers. */ private fun onSendMessage(intent: Intent) { - val status = intent.getIntExtra(EXTRA_STATUS, 0) + val statusOrd = intent.getIntExtra(EXTRA_STATUS, IQMessageStatus.FAILURE_UNKNOWN.ordinal) + val status = IQMessageStatus.values().firstOrNull { s -> s.ordinal == statusOrd } ?: IQMessageStatus.FAILURE_UNKNOWN val deviceId = getDevice(intent) - val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.lowercase() + val appId = intent.getStringExtra(EXTRA_APPLICATION_ID)?.uppercase() if (deviceId == null || appId == null) { aapsLogger.warn(LTag.GARMIN, "onSendMessage device='$deviceId' app='$appId'") } else { @@ -203,21 +196,22 @@ class GarminDeviceClient( return } + var errorMessage: String? = null when (status) { - IQMessage.FAILURE_DEVICE_NOT_CONNECTED, - IQMessage.FAILURE_DURING_TRANSFER -> { + IQMessageStatus.SUCCESS -> {} + IQMessageStatus.FAILURE_DEVICE_NOT_CONNECTED, + IQMessageStatus.FAILURE_DURING_TRANSFER -> { if (msg.attempt < MAX_RETRIES) { val delaySec = retryWaitFactor * msg.attempt Schedulers.io().scheduleDirect({ retryMessage(deviceId, appId) }, delaySec, TimeUnit.SECONDS) return } } - - else -> {} + else -> { + errorMessage = "error $status" + } } - queue.remove(msg) - val errorMessage = status - .takeUnless { it == IQMessage.SUCCESS }?.let { s -> "error $s" } + queue.poll() receiver.onSendMessage(this, msg.app.device.id, msg.app.id, errorMessage) if (queue.isNotEmpty()) { Schedulers.io().scheduleDirect { retryMessage(deviceId, appId) } @@ -237,8 +231,9 @@ class GarminDeviceClient( val app: GarminApplication, val data: ByteArray) { var attempt: Int = 0 + val creation = Instant.now() var lastAttempt: Instant? = null - val iqApp get() = IQApp().apply { applicationID = app.id; displayName = app.name } + val iqApp get() = IQApp(app.id, app.name, 0) val iqDevice get() = app.device.toIQDevice() } @@ -247,7 +242,17 @@ class GarminDeviceClient( override fun sendMessage(app: GarminApplication, data: ByteArray) { val msg = synchronized (messageQueues) { val msg = Message(app, data) + val oldMessageCutOff = Instant.now().minusSeconds(30) val queue = messageQueues.getOrPut(app.device.id to app.id) { LinkedList() } + while (true) { + val oldMsg = queue.peek() ?: break + if ((oldMsg.lastAttempt ?: oldMsg.creation).isBefore(oldMessageCutOff)) { + aapsLogger.warn(LTag.GARMIN, "remove old msg ${msg.app}") + queue.poll() + } else { + break + } + } queue.add(msg) // Make sure we have only one outstanding message per app, so we ensure // that always the first message in the queue is currently send. @@ -266,10 +271,7 @@ class GarminDeviceClient( private fun sendMessage(msg: Message) { msg.attempt++ msg.lastAttempt = Instant.now() - val iqMsg = IQMessage().apply { - messageData = msg.data - notificationPackage = context.packageName - notificationAction = sendMessageAction } + val iqMsg = IQMessage(msg.data, context.packageName, sendMessageAction) ciqService?.sendMessage(iqMsg, msg.iqDevice, msg.iqApp) } @@ -278,7 +280,6 @@ class GarminDeviceClient( companion object { const val CONNECTIQ_SERVICE_ACTION = "com.garmin.android.apps.connectmobile.CONNECTIQ_SERVICE_ACTION" const val EXTRA_APPLICATION_ID = "com.garmin.android.connectiq.EXTRA_APPLICATION_ID" - const val EXTRA_APPLICATION_VERSION = "com.garmin.android.connectiq.EXTRA_APPLICATION_VERSION" const val EXTRA_REMOTE_DEVICE = "com.garmin.android.connectiq.EXTRA_REMOTE_DEVICE" const val EXTRA_PAYLOAD = "com.garmin.android.connectiq.EXTRA_PAYLOAD" const val EXTRA_STATUS = "com.garmin.android.connectiq.EXTRA_STATUS" diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt index 8f8671a6ec..feaf1743fc 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminMessenger.kt @@ -4,7 +4,6 @@ import android.content.Context import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import io.reactivex.rxjava3.disposables.Disposable -import org.jetbrains.annotations.VisibleForTesting class GarminMessenger( private val aapsLogger: AAPSLogger, @@ -17,8 +16,6 @@ class GarminMessenger( private var disposed: Boolean = false /** All devices that where connected since this instance was created. */ private val devices = mutableMapOf() - @VisibleForTesting - val liveApplications = mutableSetOf() private val clients = mutableListOf() private val appIdNames = mutableMapOf() init { @@ -33,20 +30,14 @@ class GarminMessenger( private fun getDevice(client: GarminClient, deviceId: Long): GarminDevice { synchronized (devices) { - return devices.getOrPut(deviceId) { GarminDevice(client, deviceId, "unknown") } + return devices.getOrPut(deviceId) { + client.connectedDevices.firstOrNull { d -> d.id == deviceId } ?: + GarminDevice(client, deviceId, "unknown") } } } private fun getApplication(client: GarminClient, deviceId: Long, appId: String): GarminApplication { - synchronized (liveApplications) { - var app = liveApplications.firstOrNull { app -> - app.client == client && app.device.id == deviceId && app.id == appId } - if (app == null) { - app = GarminApplication(client, getDevice(client, deviceId), appId, appIdNames[appId]) - liveApplications.add(app) - } - return app - } + return GarminApplication(getDevice(client, deviceId), appId, appIdNames[appId]) } private fun startDeviceClient() { @@ -61,45 +52,18 @@ class GarminMessenger( override fun onDisconnect(client: GarminClient) { aapsLogger.info(LTag.GARMIN, "onDisconnect ${client.name}") clients.remove(client) - synchronized (liveApplications) { - liveApplications.removeIf { app -> app.client == client } + synchronized (devices) { + val deviceIds = devices.filter { (_, d) -> d.client == client }.map { (id, _) -> id } + deviceIds.forEach { id -> devices.remove(id) } } client.dispose() - when (client.name) { - "Device" -> startDeviceClient() - "Sim"-> GarminSimulatorClient(aapsLogger, this) + when (client) { + is GarminDeviceClient -> startDeviceClient() + is GarminSimulatorClient -> GarminSimulatorClient(aapsLogger, this) else -> aapsLogger.warn(LTag.GARMIN, "onDisconnect unknown client $client") } } - /** Receives notifications that a device has connected. - * - * It will retrieve status information for all applications we care about (in [appIdNames]). */ - override fun onConnectDevice(client: GarminClient, deviceId: Long, deviceName: String) { - val device = getDevice(client, deviceId).apply { name = deviceName } - aapsLogger.info(LTag.GARMIN, "onConnectDevice $device") - appIdNames.forEach { (id, name) -> client.retrieveApplicationInfo(device, id, name) } - } - - /** Receives notifications about disconnection of a device. */ - override fun onDisconnectDevice(client: GarminClient, deviceId: Long) { - val device = getDevice(client, deviceId) - aapsLogger.info(LTag.GARMIN,"onDisconnectDevice $device") - synchronized (liveApplications) { - liveApplications.removeIf { app -> app.device == device } - } - } - - /** Receives notification about applications that are installed/uninstalled - * on a device from the client. */ - override fun onApplicationInfo(device: GarminDevice, appId: String, isInstalled: Boolean) { - val app = getApplication(device.client, device.id, appId) - aapsLogger.info(LTag.GARMIN, "onApplicationInfo add $app ${if (isInstalled) "" else "un"}installed") - if (!isInstalled) { - synchronized (liveApplications) { liveApplications.remove(app) } - } - } - override fun onReceiveMessage(client: GarminClient, deviceId: Long, appId: String, data: ByteArray) { val app = getApplication(client, deviceId, appId) val msg = GarminSerializer.deserialize(data) @@ -118,14 +82,14 @@ class GarminMessenger( } fun sendMessage(device: GarminDevice, msg: Any) { - liveApplications - .filter { a -> a.device.id == device.id } - .forEach { a -> sendMessage(a, msg) } + appIdNames.forEach { (appId, _) -> + sendMessage(getApplication(device.client, device.id, appId), msg) + } } /** Sends a message to all applications on all devices. */ fun sendMessage(msg: Any) { - liveApplications.forEach { app -> sendMessage(app, msg) } + clients.forEach { cl -> cl.connectedDevices.forEach { d -> sendMessage(d, msg) }} } private fun sendMessage(app: GarminApplication, msg: Any) { @@ -139,7 +103,7 @@ class GarminMessenger( msg.toString() } val data = GarminSerializer.serialize(msg) - aapsLogger.info(LTag.GARMIN, "sendMessage $app $app ${data.size} bytes $s") + aapsLogger.info(LTag.GARMIN, "sendMessage $app ${data.size} bytes $s") try { app.client.sendMessage(app, data) } catch (e: IllegalStateException) { diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index ce67a6e8bb..6981d3d58e 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -64,12 +64,12 @@ class GarminPlugin @Inject constructor( /** Garmin ConnectIQ application id for native communication. Phone pushes values. */ private val glucoseAppIds = mapOf( - "c9e90ee7e6924829a8b45e7dafff5cb4" to "GlucoseWatch_Dev", - "1107ca6c2d5644b998d4bcb3793f2b7c" to "GlucoseDataField_Dev", - "928fe19a4d3a4259b50cb6f9ddaf0f4a" to "GlucoseWidget_Dev", - "662dfcf7f5a147de8bd37f09574adb11" to "GlucoseWatch", - "815c7328c21248c493ad9ac4682fe6b3" to "GlucoseDataField", - "4bddcc1740084a1fab83a3b2e2fcf55b" to "GlucoseWidget", + "C9E90EE7E6924829A8B45E7DAFFF5CB4" to "GlucoseWatch_Dev", + "1107CA6C2D5644B998D4BCB3793F2B7C" to "GlucoseDataField_Dev", + "928FE19A4D3A4259B50CB6F9DDAF0F4A" to "GlucoseWidget_Dev", + "662DFCF7F5A147DE8BD37F09574ADB11" to "GlucoseWatch", + "815C7328C21248C493AD9AC4682FE6B3" to "GlucoseDataField", + "4BDDCC1740084A1FAB83A3B2E2FCF55B" to "GlucoseWidget", ) @VisibleForTesting @@ -90,9 +90,7 @@ class GarminPlugin @Inject constructor( "communication_debug_mode" -> setupGarminMessenger() "communication_http", "communication_http_port" -> setupHttpServer() "garmin_aaps_key" -> sendPhoneAppMessage() - else -> return } - aapsLogger.info(LTag.GARMIN, "preferences change ${event.changedKey}") } private fun setupGarminMessenger() { @@ -121,7 +119,8 @@ class GarminPlugin @Inject constructor( .subscribe(::onNewBloodGlucose) ) setupHttpServer() - // setupGarminMessenger() + if (garminAapsKey.isNotEmpty()) + setupGarminMessenger() } fun setupHttpServer() { @@ -332,7 +331,7 @@ class GarminPlugin @Inject constructor( if (test) return if (avg > 10 && samplingStart > Instant.ofEpochMilli(0L) && samplingEnd > samplingStart) { loopHub.storeHeartRate(samplingStart, samplingEnd, avg, device) - } else { + } else if (avg > 0) { aapsLogger.warn(LTag.GARMIN, "Skip saving invalid HR $avg $samplingStart..$samplingEnd") } } diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt index c308e4bead..e8e93bac51 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminReceiver.kt @@ -11,19 +11,6 @@ interface GarminReceiver { fun onConnect(client: GarminClient) fun onDisconnect(client: GarminClient) - /** - * Notifies that a device is connected. This will be called for all connected devices - * initially. - */ - fun onConnectDevice(client: GarminClient, deviceId: Long, deviceName: String) - fun onDisconnectDevice(client: GarminClient, deviceId: Long) - - /** - * Provides application info after a call to - * {@link ConnectIqClient#retrieveApplicationInfo retrieveApplicationInfo}. - */ - fun onApplicationInfo(device: GarminDevice, appId: String, isInstalled: Boolean) - /** * Delivers received device app messages. */ diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt index 56b1ce1449..75f8d8e436 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt @@ -3,6 +3,7 @@ package app.aaps.plugins.sync.garmin import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import com.garmin.android.connectiq.IQApp +import com.garmin.android.connectiq.IQApp.IQAppStatus import io.reactivex.rxjava3.disposables.Disposable import org.jetbrains.annotations.VisibleForTesting import java.io.InputStream @@ -35,25 +36,23 @@ class GarminSimulatorClient( private val connections: MutableList = Collections.synchronizedList(mutableListOf()) private var nextDeviceId = AtomicLong(1) @VisibleForTesting - val iqApp = IQApp().apply { - applicationID = "SimApp" - status = GarminApplication.Status.INSTALLED.ordinal - displayName = "Simulator" - version = 1 } + val iqApp = IQApp("SimApp", IQAppStatus.INSTALLED, "Simulator", 1) private val readyLock = ReentrantLock() private val readyCond = readyLock.newCondition() + override val connectedDevices: List get() = connections.map { c -> c.device } + + override fun registerForMessages(app: GarminApplication) { + } private inner class Connection(private val socket: Socket): Disposable { val device = GarminDevice( this@GarminSimulatorClient, nextDeviceId.getAndAdd(1L), - "Sim@${socket.remoteSocketAddress}", - GarminDevice.Status.CONNECTED) + "Sim@${socket.remoteSocketAddress}") fun start() { executor.execute { try { - receiver.onConnectDevice(this@GarminSimulatorClient, device.id, device.name) run() } catch (e: Throwable) { aapsLogger.error(LTag.GARMIN, "$device failed", e) @@ -79,7 +78,7 @@ class GarminSimulatorClient( val data = readAvailable(socket.inputStream) ?: break if (data.isNotEmpty()) { kotlin.runCatching { - receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationID, data) + receiver.onReceiveMessage(this@GarminSimulatorClient, device.id, iqApp.applicationId, data) } } } catch (e: SocketException) { @@ -89,7 +88,6 @@ class GarminSimulatorClient( } aapsLogger.info(LTag.GARMIN, "disconnect ${device.name}" ) connections.remove(this) - receiver.onDisconnectDevice(this@GarminSimulatorClient, device.id) } private fun readAvailable(input: InputStream): ByteArray? { @@ -162,10 +160,6 @@ class GarminSimulatorClient( override fun isDisposed() = serverSocket.isClosed - override fun retrieveApplicationInfo(device: GarminDevice, appId: String, appName: String) { - receiver.onApplicationInfo(device, appId, appId == iqApp.applicationID) - } - private fun getConnection(device: GarminDevice): Connection? { return connections.firstOrNull { c -> c.device.id == device.id } } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt index 52962aebcb..3497e4c900 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminMessengerTest.kt @@ -8,135 +8,103 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.mockito.kotlin.stub import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import java.util.LinkedList -import java.util.Queue class GarminMessengerTest: TestBase() { private val context = mock() - private val client1 = mock() { - on { name } doReturn "Mock1" - } - private val client2 = mock() { - on { name } doReturn "Mock2" - } + private var appId1 = "appId1" private val appId2 = "appId2" private val apps = mapOf(appId1 to "$appId1-name", appId2 to "$appId2-name") - private val msgs: Queue> = LinkedList() + private val outMessages = mutableListOf>() + private val inMessages = mutableListOf>() private var messenger = GarminMessenger( - aapsLogger, context, apps, { app, msg -> msgs.add(app to msg) }, false, false) - private val deviceId = 11L - private val deviceName = "$deviceId-name" - private val device = GarminDevice(client1, deviceId, deviceName) + aapsLogger, context, apps, { app, msg -> inMessages.add(app to msg) }, + enableConnectIq = false, enableSimulator = false) + private val client1 = mock() { + on { name } doReturn "Mock1" + on { sendMessage(any(), any()) } doAnswer { a -> + outMessages.add(a.getArgument(0) to a.getArgument(1)) + Unit + } + } + private val client2 = mock() { + on { name } doReturn "Mock2" + on { sendMessage(any(), any()) } doAnswer { a -> + outMessages.add(a.getArgument(0) to a.getArgument(1)) + Unit + } + } + private val device1 = GarminDevice(client1, 11L, "dev1-name") private val device2 = GarminDevice(client2, 12L, "dev2-name") @BeforeEach fun setup() { messenger.onConnect(client1) messenger.onConnect(client2) + client1.stub { + on { connectedDevices } doReturn listOf(device1) + } + client2.stub { + on { connectedDevices } doReturn listOf(device2) + } } @AfterEach fun cleanup() { messenger.dispose() + verify(client1).dispose() + verify(client2).dispose() assertTrue(messenger.isDisposed) } - @Test - fun onConnectDevice() { - messenger.onConnectDevice(client1, deviceId, deviceName) - verify(client1).retrieveApplicationInfo(device, appId1, apps[appId1]!!) - verify(client1).retrieveApplicationInfo(device, appId2, apps[appId2]!!) - } - - @Test - fun onApplicationInfo() { - messenger.onApplicationInfo(device, appId1, true) - val app = messenger.liveApplications.first() - assertEquals(device, app.device) - assertEquals(appId1, app.id) - assertEquals(apps[appId1], app.name) - - messenger.onApplicationInfo(device, appId1, false) - assertEquals(0, messenger.liveApplications.size) - } - - @Test - fun onDisconnectDevice() { - messenger.onConnectDevice(client1, deviceId, deviceName) - messenger.onApplicationInfo(device, appId1, true) - messenger.onApplicationInfo(device2, appId1, true) - assertEquals(2, messenger.liveApplications.size) - messenger.onDisconnectDevice(client1, device2.id) - assertEquals(1, messenger.liveApplications.size) - assertEquals(appId1, messenger.liveApplications.first().id) - } - @Test fun onDisconnect() { - messenger.onApplicationInfo(device, appId1, true) - messenger.onApplicationInfo(device2, appId2, true) - assertEquals(2, messenger.liveApplications.size) messenger.onDisconnect(client1) - assertEquals(1, messenger.liveApplications.size) - val app = messenger.liveApplications.first() - assertEquals(device2, app.device) - assertEquals(appId2, app.id) - assertEquals(apps[appId2], app.name) + val msg = "foo" + messenger.sendMessage(msg) + outMessages.forEach { (app, payload) -> + assertEquals(client2, app.device.client) + assertEquals(msg, GarminSerializer.deserialize(payload)) + } } @Test fun onReceiveMessage() { val data = GarminSerializer.serialize("foo") - messenger.onReceiveMessage(client1, device.id, appId1, data) - val (app, payload) = msgs.remove() + messenger.onReceiveMessage(client1, device1.id, appId1, data) + val (app, payload) = inMessages.removeAt(0) assertEquals(appId1, app.id) assertEquals("foo", payload) } @Test fun sendMessageDevice() { - messenger.onApplicationInfo(device, appId1, true) - messenger.onApplicationInfo(device, appId2, true) - - val msgs = mutableListOf>() - whenever(client1.sendMessage(any(), any())).thenAnswer { i -> - msgs.add(i.getArgument(0) to i.getArgument(1)) - } - - messenger.sendMessage(device, "foo") - assertEquals(2, msgs.size) - val msg1 = msgs.first { (app, _) -> app.id == appId1 }.second - val msg2 = msgs.first { (app, _) -> app.id == appId2 }.second + messenger.sendMessage(device1, "foo") + assertEquals(2, outMessages.size) + val msg1 = outMessages.first { (app, _) -> app.id == appId1 }.second + val msg2 = outMessages.first { (app, _) -> app.id == appId2 }.second assertEquals("foo", GarminSerializer.deserialize(msg1)) assertEquals("foo", GarminSerializer.deserialize(msg2)) - messenger.onSendMessage(client1, device.id, appId1, null) + messenger.onSendMessage(client1, device1.id, appId1, null) } @Test fun onSendMessageAll() { - messenger.onApplicationInfo(device, appId1, true) - messenger.onApplicationInfo(device2, appId2, true) - assertEquals(2, messenger.liveApplications.size) - - val msgs = mutableListOf>() - whenever(client1.sendMessage(any(), any())).thenAnswer { i -> - msgs.add(i.getArgument(0) to i.getArgument(1)) - } - whenever(client2.sendMessage(any(), any())).thenAnswer { i -> - msgs.add(i.getArgument(0) to i.getArgument(1)) - } - messenger.sendMessage(listOf("foo")) - assertEquals(2, msgs.size) - val msg1 = msgs.first { (app, _) -> app.id == appId1 }.second - val msg2 = msgs.first { (app, _) -> app.id == appId2 }.second - assertEquals(listOf("foo"), GarminSerializer.deserialize(msg1)) - assertEquals(listOf("foo"), GarminSerializer.deserialize(msg2)) - messenger.onSendMessage(client1, device.id, appId1, null) + assertEquals(4, outMessages.size) + val msg11 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId1 }.second + val msg12 = outMessages.first { (app, _) -> app.device == device1 && app.id == appId2 }.second + val msg21 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId1 }.second + val msg22 = outMessages.first { (app, _) -> app.device == device2 && app.id == appId2 }.second + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg11)) + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg12)) + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg21)) + assertEquals(listOf("foo"), GarminSerializer.deserialize(msg22)) + messenger.onSendMessage(client1, device1.id, appId1, null) } } \ No newline at end of file diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt index b713af0f05..997213304a 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt @@ -1,13 +1,10 @@ package app.aaps.plugins.sync.garmin import app.aaps.shared.tests.TestBase -import org.junit.jupiter.api.Assertions.assertNotNull +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.kotlin.any -import org.mockito.kotlin.argThat -import org.mockito.kotlin.doAnswer import org.mockito.kotlin.eq import org.mockito.kotlin.isNull import org.mockito.kotlin.mock @@ -19,36 +16,14 @@ import java.time.Duration class GarminSimulatorClientTest: TestBase() { - private var device: GarminDevice? = null - private var app: GarminApplication? = null private lateinit var client: GarminSimulatorClient - private val receiver: GarminReceiver = mock() { - on { onConnectDevice(any(), any(), any()) }.doAnswer { i -> - device = GarminDevice(client, i.getArgument(1), i.getArgument(2)) - app = GarminApplication( - client, device!!, client.iqApp.applicationID, client.iqApp.displayName) - } - } + private val receiver: GarminReceiver = mock() @BeforeEach fun setup() { client = GarminSimulatorClient(aapsLogger, receiver, 0) } - @Test - fun retrieveApplicationInfo() { - assertTrue(client.awaitReady(Duration.ofSeconds(10))) - val port = client.port - val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) - Socket(ip, port).use { socket -> - assertTrue(socket.isConnected) - verify(receiver).onConnect(client) - verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) - client.retrieveApplicationInfo(app!!.device, app!!.id, app!!.name!!) - } - verify(receiver).onApplicationInfo(app!!.device, app!!.id, true) - } - @Test fun receiveMessage() { val payload = "foo".toByteArray() @@ -60,11 +35,11 @@ class GarminSimulatorClientTest: TestBase() { socket.getOutputStream().write(payload) socket.getOutputStream().flush() verify(receiver).onConnect(client) - verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) } - verify(receiver, timeout(1_000)).onReceiveMessage( - eq(client), eq(device!!.id), eq("SimApp"), - argThat { p -> payload.contentEquals(p) }) + assertEquals(1, client.connectedDevices.size) + val device: GarminDevice = client.connectedDevices.first() + verify(receiver, timeout(1_000)) + .onReceiveMessage(eq(client), eq(device.id), eq("SIMAPP"), eq(payload)) } @Test @@ -73,14 +48,16 @@ class GarminSimulatorClientTest: TestBase() { assertTrue(client.awaitReady(Duration.ofSeconds(10))) val port = client.port val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) + val device: GarminDevice + val app: GarminApplication Socket(ip, port).use { socket -> assertTrue(socket.isConnected) verify(receiver).onConnect(client) - verify(receiver, timeout(1_000)).onConnectDevice(eq(client), any(), any()) - assertNotNull(device) - assertNotNull(app) - client.sendMessage(app!!, payload) + assertEquals(1, client.connectedDevices.size) + device = client.connectedDevices.first() + app = GarminApplication(device, "SIMAPP", "T") + client.sendMessage(app, payload) } - verify(receiver).onSendMessage(eq(client), any(), eq(app!!.id), isNull()) + verify(receiver, timeout(1_000)).onSendMessage(eq(client), eq(device.id), eq(app.id), isNull()) } } \ No newline at end of file From 4d233e46792a78c544a9576d17d52060c8c3d798 Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Fri, 24 Nov 2023 12:24:15 +0100 Subject: [PATCH 13/70] Implement /sgv.json endpoint to support phone apps that use nightscout or xdrip endpoints. --- .../aaps/plugins/sync/garmin/GarminPlugin.kt | 55 +++++++++++++++++ .../plugins/sync/garmin/GarminPluginTest.kt | 60 ++++++++++++++++++- 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index 5d20fd3c70..81b03f98c5 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -14,10 +14,14 @@ 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.JsonArray import com.google.gson.JsonObject import dagger.android.HasAndroidInjector import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.schedulers.Schedulers +import java.math.BigDecimal +import java.math.MathContext +import java.math.RoundingMode import java.net.SocketAddress import java.net.URI import java.time.Clock @@ -96,6 +100,7 @@ class GarminPlugin @Inject constructor( registerEndpoint("/get", ::onGetBloodGlucose) registerEndpoint("/carbs", ::onPostCarbs) registerEndpoint("/connect", ::onConnectPump) + registerEndpoint("/sgv.json", ::onSgv) } } else if (server != null) { aapsLogger.info(LTag.GARMIN, "stopping HTTP server") @@ -277,4 +282,54 @@ class GarminPlugin @Inject constructor( jo.addProperty("connected", loopHub.isConnected) return jo.toString() } + + private fun glucoseSlopeMgDlPerMilli(glucose1: GlucoseValue, glucose2: GlucoseValue): Double { + return (glucose2.value - glucose1.value) / (glucose2.timestamp - glucose1.timestamp) + } + + /** Returns glucose values in Nightscout/Xdrip format. */ + @VisibleForTesting + @Suppress("UNUSED_PARAMETER") + fun onSgv(call: SocketAddress, uri: URI, requestBody: String?): CharSequence { + val count = getQueryParameter(uri,"count", 24L) + .toInt().coerceAtMost(1000).coerceAtLeast(1) + val briefMode = getQueryParameter(uri, "brief_mode", false) + + // Guess a start time to get [count+1] readings. This is a heuristic that only works if we get readings + // every 5 minutes and we're not missing readings. We truncate in case we get more readings but we'll + // get less, e.g., in case we're missing readings for the last half hour. We get one extra reading, + // to compute the glucose delta. + val from = clock.instant().minus(Duration.ofMinutes(5L * (count + 1))) + val glucoseValues = loopHub.getGlucoseValues(from, false) + val joa = JsonArray() + for (i in 0 until count.coerceAtMost(glucoseValues.size)) { + val jo = JsonObject() + val glucose = glucoseValues[i] + if (!briefMode) { + jo.addProperty("_id", glucose.id.toString()) + jo.addProperty("device", glucose.sourceSensor.toString()) + val timestamp = Instant.ofEpochMilli(glucose.timestamp) + jo.addProperty("deviceString", timestamp.toString()) + jo.addProperty("sysTime", timestamp.toString()) + glucose.raw?.let { raw -> jo.addProperty("unfiltered", raw) } + } + jo.addProperty("date", glucose.timestamp) + jo.addProperty("sgv", glucose.value.roundToInt()) + if (i + 1 < glucoseValues.size) { + // Compute the 5 minute delta. + val delta = 300_000.0 * glucoseSlopeMgDlPerMilli(glucoseValues[i + 1], glucose) + jo.addProperty("delta", BigDecimal(delta, MathContext(3, RoundingMode.HALF_UP))) + } + jo.addProperty("direction", glucose.trendArrow.text) + glucose.noise?.let { n -> jo.addProperty("noise", n) } + if (i == 0) { + when (loopHub.glucoseUnit) { + GlucoseUnit.MGDL -> jo.addProperty("units_hint", "mgdl") + GlucoseUnit.MMOL -> jo.addProperty("units_hint", "mmol") + } + } + joa.add(jo) + } + return joa.toString() + } } diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt index 18c772877e..6913ad4609 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt @@ -21,6 +21,10 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` +import org.mockito.kotlin.any +import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.eq +import org.mockito.kotlin.whenever import java.net.SocketAddress import java.net.URI import java.time.Clock @@ -28,6 +32,7 @@ import java.time.Instant import java.time.ZoneId import java.time.temporal.ChronoUnit import java.util.concurrent.locks.Condition +import kotlin.ranges.LongProgression.Companion.fromClosedRange class GarminPluginTest: TestBase() { private lateinit var gp: GarminPlugin @@ -71,8 +76,9 @@ class GarminPluginTest: TestBase() { "device" to "Test_Device") private fun createGlucoseValue(timestamp: Instant, value: Double = 93.0) = GlucoseValue( + id = 10 * timestamp.toEpochMilli(), timestamp = timestamp.toEpochMilli(), raw = 90.0, value = value, - trendArrow = GlucoseValue.TrendArrow.FLAT, noise = null, + trendArrow = GlucoseValue.TrendArrow.FLAT, noise = 4.5, sourceSensor = GlucoseValue.SourceSensor.RANDOM ) @@ -205,4 +211,56 @@ class GarminPluginTest: TestBase() { verify(loopHub).connectPump() verify(loopHub).isConnected } + + @Test + fun onSgv_NoGlucose() { + whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL) + whenever(loopHub.getGlucoseValues(any(), eq(false))).thenReturn(emptyList()) + assertEquals("[]", gp.onSgv(mock(), createUri(mapOf()), null)) + verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false) + } + + @Test + fun onSgv_NoDelta() { + whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL) + whenever(loopHub.getGlucoseValues(any(), eq(false))).thenReturn( + listOf(createGlucoseValue( + clock.instant().minusSeconds(100L), 99.3))) + assertEquals( + """[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol"}]""", + gp.onSgv(mock(), createUri(mapOf()), null)) + verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false) + verify(loopHub).glucoseUnit + } + + @Test + fun onSgv() { + whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL) + whenever(loopHub.getGlucoseValues(any(), eq(false))).thenAnswer { i -> + val from = i.getArgument(0) + fromClosedRange(from.toEpochMilli(), clock.instant().toEpochMilli(), 300_000L) + .map(Instant::ofEpochMilli) + .mapIndexed { idx, ts -> createGlucoseValue(ts, 100.0+(10 * idx)) }.reversed()} + assertEquals( + """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol"}]""", + gp.onSgv(mock(), createUri(mapOf("count" to "1")), null)) + verify(loopHub).getGlucoseValues( + clock.instant().minusSeconds(600L), false) + + assertEquals( + """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol"},""" + + """{"_id":"-2900000","device":"RANDOM","deviceString":"1969-12-31T23:55:10Z","sysTime":"1969-12-31T23:55:10Z","unfiltered":90.0,"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", + gp.onSgv(mock(), createUri(mapOf("count" to "2")), null)) + verify(loopHub).getGlucoseValues( + clock.instant().minusSeconds(900L), false) + + assertEquals( + """[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol"},""" + + """{"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", + gp.onSgv(mock(), createUri(mapOf("count" to "2", "brief_mode" to "true")), null)) + verify(loopHub, times(2)).getGlucoseValues( + clock.instant().minusSeconds(900L), false) + + verify(loopHub, atLeastOnce()).glucoseUnit + } } From e29d8f59f9eef24c9bccfae24693590fc9ad0d7d Mon Sep 17 00:00:00 2001 From: swissalpine Date: Sun, 26 Nov 2023 12:32:00 +0100 Subject: [PATCH 14/70] Garmin sgv.json: add iob, cob and tbr --- .../main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index 81b03f98c5..a65cdbee14 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -327,6 +327,9 @@ class GarminPlugin @Inject constructor( GlucoseUnit.MGDL -> jo.addProperty("units_hint", "mgdl") GlucoseUnit.MMOL -> jo.addProperty("units_hint", "mmol") } + jo.addProperty("iob", loopHub.insulinTotalOnboard) + jo.addProperty("tbr", loopHub.temporaryBasalPercent) + jo.addProperty("cob", loopHub.carbsOnboard) } joa.add(jo) } From beac0ecf1c3889e3850d70c8cadf82a87085a21c Mon Sep 17 00:00:00 2001 From: swissalpine Date: Sun, 26 Nov 2023 12:34:43 +0100 Subject: [PATCH 15/70] Update LoopHub.kt: add iob, cob, tbr val --- .../kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt index 4b420d21f5..f2dc1b8a7f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt @@ -20,6 +20,12 @@ interface LoopHub { /** Returns the remaining bolus insulin on board. */ val insulinOnboard: Double + /** Returns the remaining bolus and basal insulin on board. */ + val insulinTotalOnboard: Double + + /** Returns the remaining carbs on board. */ + val carbsOnboard: Double? + /** Returns true if the pump is connected. */ val isConnected: Boolean @@ -29,6 +35,9 @@ interface LoopHub { /** Returns the factor by which the basal rate is currently raised (> 1) or lowered (< 1). */ val temporaryBasal: Double + /** Returns the temporary basal rate in percent */ + val temporaryBasalPercent: String + /** Tells the loop algorithm that the pump is physically connected. */ fun connectPump() @@ -48,4 +57,4 @@ interface LoopHub { avgHeartRate: Int, device: String? ) -} \ No newline at end of file +} From b7dfa735c536d658d5b344ae17178947c92c25e6 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Sun, 26 Nov 2023 12:35:21 +0100 Subject: [PATCH 16/70] Update LoopHubImpl.kt: add iob, cob, tbr --- .../aaps/plugins/sync/garmin/LoopHubImpl.kt | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt index dfcf9dab37..0eabe78e4f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt @@ -13,6 +13,7 @@ import app.aaps.core.interfaces.profile.ProfileFunction import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.sharedPreferences.SP +import app.aaps.core.main.graph.OverviewData import app.aaps.database.ValueWrapper import app.aaps.database.entities.EffectiveProfileSwitch import app.aaps.database.entities.GlucoseValue @@ -42,6 +43,7 @@ class LoopHubImpl @Inject constructor( private val repo: AppRepository, private val userEntryLogger: UserEntryLogger, private val sp: SP, + private val overviewData: OverviewData, ) : LoopHub { @VisibleForTesting @@ -64,6 +66,15 @@ class LoopHubImpl @Inject constructor( override val insulinOnboard: Double get() = iobCobCalculator.calculateIobFromBolus().iob + /** Returns the remaining bolus and basal insulin on board. */ + override val insulinTotalOnboard :Double + get() = iobCobCalculator.calculateIobFromBolus().iob + iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().basaliob + + /** Returns the remaining carbs on board. */ + override val carbsOnboard: Double? + // get() = overviewData.cobInfo(iobCobCalculator).displayText(rh, decimalFormatter) ?: rh.gs(app.aaps.core.ui.R.string.value_unavailable_short) + get() = overviewData.cobInfo(iobCobCalculator).displayCob + /** Returns true if the pump is connected. */ override val isConnected: Boolean get() = !loop.isDisconnected @@ -83,6 +94,13 @@ class LoopHubImpl @Inject constructor( return if (apsResult == null) Double.NaN else apsResult.percent / 100.0 } + /** Returns the temporary basal rate in percent */ + override val temporaryBasalPercent: String + get() { + val apsResult = loop.lastRun?.constraintsProcessed + return if (apsResult == null) "--" else apsResult.percent.toString() + } + /** Tells the loop algorithm that the pump is physicallly connected. */ override fun connectPump() { repo.runTransaction( @@ -142,4 +160,4 @@ class LoopHubImpl @Inject constructor( ) repo.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait() } -} \ No newline at end of file +} From a35a5b8be0d9bb046214e5378bfdd4d093438505 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Sun, 26 Nov 2023 14:29:31 +0100 Subject: [PATCH 17/70] Code clean up --- .../src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt index 0eabe78e4f..aa8c3f7021 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt @@ -72,7 +72,6 @@ class LoopHubImpl @Inject constructor( /** Returns the remaining carbs on board. */ override val carbsOnboard: Double? - // get() = overviewData.cobInfo(iobCobCalculator).displayText(rh, decimalFormatter) ?: rh.gs(app.aaps.core.ui.R.string.value_unavailable_short) get() = overviewData.cobInfo(iobCobCalculator).displayCob /** Returns true if the pump is connected. */ From 3fcdad527518167c12d3a77fd7dd54946ef252bd Mon Sep 17 00:00:00 2001 From: swissalpine Date: Sun, 26 Nov 2023 22:59:56 +0100 Subject: [PATCH 18/70] Fix Garmin Pugintest (missing iob, tbr, cob) --- .../app/aaps/plugins/sync/garmin/GarminPluginTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt index 6913ad4609..64f6c6de9f 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt @@ -227,7 +227,7 @@ class GarminPluginTest: TestBase() { listOf(createGlucoseValue( clock.instant().minusSeconds(100L), 99.3))) assertEquals( - """[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol"}]""", + """[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":4.8,"tbr":"120","cob":12.1}]""", gp.onSgv(mock(), createUri(mapOf()), null)) verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false) verify(loopHub).glucoseUnit @@ -242,20 +242,20 @@ class GarminPluginTest: TestBase() { .map(Instant::ofEpochMilli) .mapIndexed { idx, ts -> createGlucoseValue(ts, 100.0+(10 * idx)) }.reversed()} assertEquals( - """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol"}]""", + """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":4.8,"tbr":"120","cob":12.1}]""", gp.onSgv(mock(), createUri(mapOf("count" to "1")), null)) verify(loopHub).getGlucoseValues( clock.instant().minusSeconds(600L), false) assertEquals( - """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol"},""" + + """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":4.8,"tbr":"120","cob":12.1},""" + """{"_id":"-2900000","device":"RANDOM","deviceString":"1969-12-31T23:55:10Z","sysTime":"1969-12-31T23:55:10Z","unfiltered":90.0,"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", gp.onSgv(mock(), createUri(mapOf("count" to "2")), null)) verify(loopHub).getGlucoseValues( clock.instant().minusSeconds(900L), false) assertEquals( - """[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol"},""" + + """[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":4.8,"tbr":"120","cob":12.1},""" + """{"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", gp.onSgv(mock(), createUri(mapOf("count" to "2", "brief_mode" to "true")), null)) verify(loopHub, times(2)).getGlucoseValues( From 3f59d4eb669294c4ed8ecc954bf7d12cd0e41f6d Mon Sep 17 00:00:00 2001 From: swissalpine Date: Sun, 26 Nov 2023 23:28:35 +0100 Subject: [PATCH 19/70] Fix tests loophubtest.kt --- .../kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt index a88facfa05..85f30ce03a 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt @@ -10,9 +10,11 @@ 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.profile.ProfileUtil import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.sharedPreferences.SP +import app.aaps.core.main.graph.OverviewData import app.aaps.database.ValueWrapper import app.aaps.database.entities.EffectiveProfileSwitch import app.aaps.database.entities.GlucoseValue @@ -54,6 +56,8 @@ class LoopHubTest: TestBase() { @Mock lateinit var repo: AppRepository @Mock lateinit var userEntryLogger: UserEntryLogger @Mock lateinit var sp: SP + @Mock lateinit var overviewData: OverviewData + @Mock lateinit var profileUtil: ProfileUtil private lateinit var loopHub: LoopHubImpl private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC")) @@ -62,7 +66,7 @@ class LoopHubTest: TestBase() { fun setup() { loopHub = LoopHubImpl( aapsLogger, commandQueue, constraints, iobCobCalculator, loop, - profileFunction, repo, userEntryLogger, sp + profileFunction, repo, userEntryLogger, sp, overviewData, profileUtil ) loopHub.clock = clock } @@ -76,6 +80,8 @@ class LoopHubTest: TestBase() { verifyNoMoreInteractions(profileFunction) verifyNoMoreInteractions(repo) verifyNoMoreInteractions(userEntryLogger) + verifyNoMoreInteractions(overviewData) + verifyNoMoreInteractions(profileUtil) } @Test @@ -247,4 +253,4 @@ class LoopHubTest: TestBase() { samplingStart, samplingEnd, 101, "Test Device") verify(repo).runTransaction(InsertOrUpdateHeartRateTransaction(hr)) } -} \ No newline at end of file +} From bc5facc623a0004a9c8d4bb987e4cc0944930985 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Mon, 27 Nov 2023 02:44:07 +0100 Subject: [PATCH 20/70] Next try to fix test: Remove obsolet line --- .../test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt index 85f30ce03a..556d76e4d6 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt @@ -10,7 +10,6 @@ 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.profile.ProfileUtil import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.queue.CommandQueue import app.aaps.core.interfaces.sharedPreferences.SP @@ -57,7 +56,6 @@ class LoopHubTest: TestBase() { @Mock lateinit var userEntryLogger: UserEntryLogger @Mock lateinit var sp: SP @Mock lateinit var overviewData: OverviewData - @Mock lateinit var profileUtil: ProfileUtil private lateinit var loopHub: LoopHubImpl private val clock = Clock.fixed(Instant.ofEpochMilli(10_000), ZoneId.of("UTC")) @@ -81,7 +79,6 @@ class LoopHubTest: TestBase() { verifyNoMoreInteractions(repo) verifyNoMoreInteractions(userEntryLogger) verifyNoMoreInteractions(overviewData) - verifyNoMoreInteractions(profileUtil) } @Test From 68720cb2da4ea84aa1e032d004367991c9a96ba6 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Mon, 27 Nov 2023 03:12:02 +0100 Subject: [PATCH 21/70] Fix Tests: One argument left to delete --- .../src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt index 556d76e4d6..bd52555191 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt @@ -64,7 +64,7 @@ class LoopHubTest: TestBase() { fun setup() { loopHub = LoopHubImpl( aapsLogger, commandQueue, constraints, iobCobCalculator, loop, - profileFunction, repo, userEntryLogger, sp, overviewData, profileUtil + profileFunction, repo, userEntryLogger, sp, overviewData ) loopHub.clock = clock } From 10846b118a59449b0b4b3d1e25a4552ccf3177d2 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 28 Nov 2023 14:20:01 +0100 Subject: [PATCH 22/70] New Crowdin updates (#3076) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Spanish) * New translations strings.xml (Spanish) --- plugins/aps/src/main/res/values-es-rES/strings.xml | 2 +- plugins/smoothing/src/main/res/values-nb-rNO/strings.xml | 6 +++--- plugins/sync/src/main/res/values-es-rES/strings.xml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/aps/src/main/res/values-es-rES/strings.xml b/plugins/aps/src/main/res/values-es-rES/strings.xml index b9b3566744..f46d421cb7 100644 --- a/plugins/aps/src/main/res/values-es-rES/strings.xml +++ b/plugins/aps/src/main/res/values-es-rES/strings.xml @@ -1,7 +1,7 @@ Habilitar la relación de sensibilidad basada en TDD para modificar las basales y el objetivo de glucosa - Utiliza las últimas 24h TDD/7D TDD para calcular el ratio de sensibilidad utilizado para aumentar o disminuir la tasa basal, y también ajustar el objetivo de glucosa si estas opciones están activadas, de la misma forma que lo hace Autosens. Se recomienda comenzar con esta opción desactivada + Utiliza las últimas 24h TDD/7D TDD para calcular el factor de sensibilidad utilizado para aumentar o disminuir la tasa basal, y también ajusta el objetivo de glucosa si estas opciones están activadas, de la misma forma que lo hace Autosens. Se recomienda comenzar con esta opción desactivada Factor de ajuste de ISF Dinámico % Factor de ajuste para ISF Dinámico. Establezca más de 100% para una corrección más agresiva, y menos de 100% para correcciones más susves. Objetivo temporal alto aumenta la sensibilidad diff --git a/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml b/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml index f24cdc1d85..36e143d917 100644 --- a/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/smoothing/src/main/res/values-nb-rNO/strings.xml @@ -2,9 +2,9 @@ UTJEVNING Eksponentiell utjevning - "Andre ordens algoritme for eksponentiell utjevning" + "Algoritme for eksponentiell utjevning, nyeste BS-verdi påvirkes" Gjennomsnittlig utjevning - "Gjennomsnittlig utjevnings-algoritme, nyeste verdi påvirkes ikke" + "Algoritme for gjennomsnittlig utjevning, nyeste BS-verdi påvirkes ikke. Kan minne om BYODA G6 sin utjevningsalgoritme" Ingen utjevning - "Ingen utjevning utføres på motatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6." + "Ingen utjevning utføres på mottatte blodsukkerverdier. Bruk dette valget når du allerede har filtrerte data, f.eks. fra BYODA G6." diff --git a/plugins/sync/src/main/res/values-es-rES/strings.xml b/plugins/sync/src/main/res/values-es-rES/strings.xml index 05ce3e3902..0c777ef050 100644 --- a/plugins/sync/src/main/res/values-es-rES/strings.xml +++ b/plugins/sync/src/main/res/values-es-rES/strings.xml @@ -128,7 +128,7 @@ Supervisar y controlar AAPS usando un reloj WearOS (Ningún reloj conectado) Estado de la bomba de insulina - Estado del lazo + Estado del bucle Calc. Asistente:\nInsulina: %1$.2fU\nCarbohidratos: %2$dg El asistente rápido seleccionado ya no está disponible, por favor actualice su tarjeta Asistente Rápido: %1$s\nInsulina: %2$.2fU\nCarbohidratos: %3$dg From 5d323950429f582d762d43806ccf260c3b5aac36 Mon Sep 17 00:00:00 2001 From: olorinmaia Date: Tue, 28 Nov 2023 22:36:51 +0100 Subject: [PATCH 23/70] Add translation strings for Dash statuses in AAPSClient pump status --- pump/omnipod-common/src/main/res/values/strings.xml | 1 + .../plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pump/omnipod-common/src/main/res/values/strings.xml b/pump/omnipod-common/src/main/res/values/strings.xml index d2ecea3ce0..0a2757f7c5 100644 --- a/pump/omnipod-common/src/main/res/values/strings.xml +++ b/pump/omnipod-common/src/main/res/values/strings.xml @@ -158,6 +158,7 @@ Activation time exceeded Inactive Pod Fault: %1$03d %2$s + Normal Deactivate Pod diff --git a/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt b/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt index 19387cc705..a0acf389de 100644 --- a/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt +++ b/pump/omnipod-dash/src/main/java/info/nightscout/androidaps/plugins/pump/omnipod/dash/OmnipodDashPumpPlugin.kt @@ -991,9 +991,9 @@ class OmnipodDashPumpPlugin @Inject constructor( val extended = JSONObject() try { val podStatus = when { - podStateManager.isPodRunning && podStateManager.isSuspended -> "suspended" - podStateManager.isPodRunning -> "normal" - else -> "no active Pod" + podStateManager.isPodRunning && podStateManager.isSuspended -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_suspended).lowercase() + podStateManager.isPodRunning -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_normal).lowercase() + else -> rh.gs(info.nightscout.androidaps.plugins.pump.omnipod.common.R.string.omnipod_common_pod_status_no_active_pod).lowercase() } status.put("status", podStatus) status.put("timestamp", dateUtil.toISOString(podStateManager.lastUpdatedSystem)) From 50c035960a7ecca19db3a121a0c3afc93ad62b88 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 29 Nov 2023 14:30:03 +0100 Subject: [PATCH 24/70] Prevent double run of wizard --- app/src/main/kotlin/app/aaps/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/app/aaps/MainActivity.kt b/app/src/main/kotlin/app/aaps/MainActivity.kt index b5296ab7c4..bf01cacc20 100644 --- a/app/src/main/kotlin/app/aaps/MainActivity.kt +++ b/app/src/main/kotlin/app/aaps/MainActivity.kt @@ -276,7 +276,7 @@ class MainActivity : DaggerAppCompatActivityWithResult() { }) // Setup views on 2nd and next activity start // On 1st start app is still initializing, start() is delayed and run from EventAppInitialized - if (config.appInitialized) start() + if (config.appInitialized) setupViews() } private fun start() { From 9b2441f4577727d173c4a1928cb4ecb1300861a2 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Thu, 30 Nov 2023 11:44:42 +0100 Subject: [PATCH 25/70] Rework tbr --- .../kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index a65cdbee14..1a37a3ae4b 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -327,8 +327,13 @@ class GarminPlugin @Inject constructor( GlucoseUnit.MGDL -> jo.addProperty("units_hint", "mgdl") GlucoseUnit.MMOL -> jo.addProperty("units_hint", "mmol") } - jo.addProperty("iob", loopHub.insulinTotalOnboard) - jo.addProperty("tbr", loopHub.temporaryBasalPercent) + jo.addProperty("iob", loopHub.insulinOnboard + loopHub.insulinBasalOnboard) + loopHub.temporaryBasal.also { + if (!it.isNaN()) { + val temporaryBasalRateInPercent = (it * 100.0).toInt() + jo.addProperty("tbr", temporaryBasalRateInPercent) + } + } jo.addProperty("cob", loopHub.carbsOnboard) } joa.add(jo) From 1e13237a5bb2dad5bd9f6a556c3d6e59a7e3dd87 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Thu, 30 Nov 2023 11:46:35 +0100 Subject: [PATCH 26/70] Rework --- .../kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt index f2dc1b8a7f..5d4ac7dbd9 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt @@ -20,8 +20,8 @@ interface LoopHub { /** Returns the remaining bolus insulin on board. */ val insulinOnboard: Double - /** Returns the remaining bolus and basal insulin on board. */ - val insulinTotalOnboard: Double + /** Returns the basal insulin on board. */ + val insulinBasalOnboard: Double /** Returns the remaining carbs on board. */ val carbsOnboard: Double? @@ -35,9 +35,6 @@ interface LoopHub { /** Returns the factor by which the basal rate is currently raised (> 1) or lowered (< 1). */ val temporaryBasal: Double - /** Returns the temporary basal rate in percent */ - val temporaryBasalPercent: String - /** Tells the loop algorithm that the pump is physically connected. */ fun connectPump() @@ -51,6 +48,9 @@ interface LoopHub { /** Notifies the system that carbs were eaten and stores the value. */ fun postCarbs(carbohydrates: Int) + /** Stores or cancels a temptarget. */ + fun postTempTarget(target: Double, duration: Int) + /** Stores hear rate readings that a taken and averaged of the given interval. */ fun storeHeartRate( samplingStart: Instant, samplingEnd: Instant, From e6c9e13d510a723d7aab1aebf018d7350b5b8de8 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Thu, 30 Nov 2023 11:48:38 +0100 Subject: [PATCH 27/70] Rework --- .../app/aaps/plugins/sync/garmin/LoopHubImpl.kt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt index aa8c3f7021..1e9f19ae7f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHubImpl.kt @@ -67,8 +67,8 @@ class LoopHubImpl @Inject constructor( get() = iobCobCalculator.calculateIobFromBolus().iob /** Returns the remaining bolus and basal insulin on board. */ - override val insulinTotalOnboard :Double - get() = iobCobCalculator.calculateIobFromBolus().iob + iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().basaliob + override val insulinBasalOnboard :Double + get() = iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended().basaliob /** Returns the remaining carbs on board. */ override val carbsOnboard: Double? @@ -93,13 +93,6 @@ class LoopHubImpl @Inject constructor( return if (apsResult == null) Double.NaN else apsResult.percent / 100.0 } - /** Returns the temporary basal rate in percent */ - override val temporaryBasalPercent: String - get() { - val apsResult = loop.lastRun?.constraintsProcessed - return if (apsResult == null) "--" else apsResult.percent.toString() - } - /** Tells the loop algorithm that the pump is physicallly connected. */ override fun connectPump() { repo.runTransaction( From 46c5e82e6cf205efb14eaf5d1525284a14c9420e Mon Sep 17 00:00:00 2001 From: swissalpine Date: Thu, 30 Nov 2023 11:49:54 +0100 Subject: [PATCH 28/70] Fix tests --- .../aaps/plugins/sync/garmin/GarminPluginTest.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt index 64f6c6de9f..a99668daae 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt @@ -57,6 +57,10 @@ class GarminPluginTest: TestBase() { @AfterEach fun verifyNoFurtherInteractions() { verify(loopHub, atMost(2)).currentProfileName + verify(loopHub, atMost(3)).insulinOnboard + verify(loopHub, atMost(3)).insulinBasalOnboard + verify(loopHub, atMost(3)).temporaryBasal + verify(loopHub, atMost(3)).carbsOnboard verifyNoMoreInteractions(loopHub) } @@ -227,8 +231,8 @@ class GarminPluginTest: TestBase() { listOf(createGlucoseValue( clock.instant().minusSeconds(100L), 99.3))) assertEquals( - """[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":4.8,"tbr":"120","cob":12.1}]""", - gp.onSgv(mock(), createUri(mapOf()), null)) + """[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":0.0,"tbr":0,"cob":0.0}]""", + gp.onSgv(mock(), createUri(mapOf()), null)) verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false) verify(loopHub).glucoseUnit } @@ -242,20 +246,20 @@ class GarminPluginTest: TestBase() { .map(Instant::ofEpochMilli) .mapIndexed { idx, ts -> createGlucoseValue(ts, 100.0+(10 * idx)) }.reversed()} assertEquals( - """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":4.8,"tbr":"120","cob":12.1}]""", + """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":0.0,"tbr":0,"cob":0.0}]""", gp.onSgv(mock(), createUri(mapOf("count" to "1")), null)) verify(loopHub).getGlucoseValues( clock.instant().minusSeconds(600L), false) assertEquals( - """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":4.8,"tbr":"120","cob":12.1},""" + - """{"_id":"-2900000","device":"RANDOM","deviceString":"1969-12-31T23:55:10Z","sysTime":"1969-12-31T23:55:10Z","unfiltered":90.0,"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", + """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":0.0,"tbr":0,"cob":0.0},""" + + """{"_id":"-2900000","device":"RANDOM","deviceString":"1969-12-31T23:55:10Z","sysTime":"1969-12-31T23:55:10Z","unfiltered":90.0,"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", gp.onSgv(mock(), createUri(mapOf("count" to "2")), null)) verify(loopHub).getGlucoseValues( clock.instant().minusSeconds(900L), false) assertEquals( - """[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":4.8,"tbr":"120","cob":12.1},""" + + """[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":0.0,"tbr":0,"cob":0.0},""" + """{"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", gp.onSgv(mock(), createUri(mapOf("count" to "2", "brief_mode" to "true")), null)) verify(loopHub, times(2)).getGlucoseValues( From dad61d5a3704806822cde51c04218308d0ec53c5 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Thu, 30 Nov 2023 11:51:36 +0100 Subject: [PATCH 29/70] Fix tests --- .../aaps/plugins/sync/garmin/LoopHubTest.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt index bd52555191..acdf58d391 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt @@ -81,7 +81,7 @@ class LoopHubTest: TestBase() { verifyNoMoreInteractions(overviewData) } - @Test +@Test fun testCurrentProfile() { val profile = mock(Profile::class.java) `when`(profileFunction.getProfile()).thenReturn(profile) @@ -112,6 +112,22 @@ class LoopHubTest: TestBase() { verify(iobCobCalculator, times(1)).calculateIobFromBolus() } + @Test + fun testBasalOnBoard() { + val iobBasal = IobTotal(time = 0).apply { basaliob = 23.9 } + `when`(iobCobCalculator.calculateIobFromTempBasalsIncludingConvertedExtended()).thenReturn(iobBasal) + assertEquals(23.9, loopHub.insulinBasalOnboard, 1e-10) + verify(iobCobCalculator, times(1)).calculateIobFromTempBasalsIncludingConvertedExtended() + } + + @Test + fun testCarbsOnBoard() { + val cobInfo = CobInfo(0, 12.0, 0.0) + `when`(overviewData.cobInfo(iobCobCalculator)).thenReturn(cobInfo) + assertEquals(12.0, loopHub.carbsOnboard) + verify(overviewData, times(1)).cobInfo(iobCobCalculator) + } + @Test fun testIsConnected() { `when`(loop.isDisconnected).thenReturn(false) From aecde2032e06b5b3f20d3cc530c047cb69794183 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Thu, 30 Nov 2023 11:54:51 +0100 Subject: [PATCH 30/70] Delete unfinished function --- .../src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt index 5d4ac7dbd9..ebd8ac209b 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/LoopHub.kt @@ -48,9 +48,6 @@ interface LoopHub { /** Notifies the system that carbs were eaten and stores the value. */ fun postCarbs(carbohydrates: Int) - /** Stores or cancels a temptarget. */ - fun postTempTarget(target: Double, duration: Int) - /** Stores hear rate readings that a taken and averaged of the given interval. */ fun storeHeartRate( samplingStart: Instant, samplingEnd: Instant, From 3ce6280e4c7220e081dae843d943f8650e769be5 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Thu, 30 Nov 2023 12:14:26 +0100 Subject: [PATCH 31/70] Add missing import --- .../src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt index acdf58d391..c96bcaee4b 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/LoopHubTest.kt @@ -5,6 +5,7 @@ import app.aaps.core.interfaces.aps.Loop import app.aaps.core.interfaces.constraints.Constraint import app.aaps.core.interfaces.constraints.ConstraintsChecker import app.aaps.core.interfaces.db.GlucoseUnit +import app.aaps.core.interfaces.iob.CobInfo import app.aaps.core.interfaces.iob.IobCobCalculator import app.aaps.core.interfaces.iob.IobTotal import app.aaps.core.interfaces.logging.UserEntryLogger From 4d504e60cdfbf722315ba9abd98c9b985a4a5088 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Thu, 30 Nov 2023 15:47:39 +0100 Subject: [PATCH 32/70] Add values to onSGV tests --- .../plugins/sync/garmin/GarminPluginTest.kt | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt index a99668daae..447788146e 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt @@ -225,13 +225,17 @@ class GarminPluginTest: TestBase() { } @Test - fun onSgv_NoDelta() { +fun onSgv_NoDelta() { whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL) + whenever(loopHub.insulinOnboard).thenReturn(2.7) + whenever(loopHub.insulinBasalOnboard).thenReturn(2.5) + whenever(loopHub.temporaryBasal).thenReturn(0.8) + whenever(loopHub.carbsOnboard).thenReturn(10.7) whenever(loopHub.getGlucoseValues(any(), eq(false))).thenReturn( listOf(createGlucoseValue( clock.instant().minusSeconds(100L), 99.3))) assertEquals( - """[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":0.0,"tbr":0,"cob":0.0}]""", + """[{"_id":"-900000","device":"RANDOM","deviceString":"1969-12-31T23:58:30Z","sysTime":"1969-12-31T23:58:30Z","unfiltered":90.0,"date":-90000,"sgv":99,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7}]""", gp.onSgv(mock(), createUri(mapOf()), null)) verify(loopHub).getGlucoseValues(clock.instant().minusSeconds(25L * 300L), false) verify(loopHub).glucoseUnit @@ -240,26 +244,31 @@ class GarminPluginTest: TestBase() { @Test fun onSgv() { whenever(loopHub.glucoseUnit).thenReturn(GlucoseUnit.MMOL) + whenever(loopHub.insulinOnboard).thenReturn(2.7) + whenever(loopHub.insulinBasalOnboard).thenReturn(2.5) + whenever(loopHub.temporaryBasal).thenReturn(0.8) + whenever(loopHub.carbsOnboard).thenReturn(10.7) whenever(loopHub.getGlucoseValues(any(), eq(false))).thenAnswer { i -> val from = i.getArgument(0) fromClosedRange(from.toEpochMilli(), clock.instant().toEpochMilli(), 300_000L) .map(Instant::ofEpochMilli) .mapIndexed { idx, ts -> createGlucoseValue(ts, 100.0+(10 * idx)) }.reversed()} assertEquals( - """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":0.0,"tbr":0,"cob":0.0}]""", + """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7}]""", gp.onSgv(mock(), createUri(mapOf("count" to "1")), null)) verify(loopHub).getGlucoseValues( clock.instant().minusSeconds(600L), false) + assertEquals( - """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":0.0,"tbr":0,"cob":0.0},""" + + """[{"_id":"100000","device":"RANDOM","deviceString":"1970-01-01T00:00:10Z","sysTime":"1970-01-01T00:00:10Z","unfiltered":90.0,"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7},""" + """{"_id":"-2900000","device":"RANDOM","deviceString":"1969-12-31T23:55:10Z","sysTime":"1969-12-31T23:55:10Z","unfiltered":90.0,"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", gp.onSgv(mock(), createUri(mapOf("count" to "2")), null)) verify(loopHub).getGlucoseValues( clock.instant().minusSeconds(900L), false) assertEquals( - """[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":0.0,"tbr":0,"cob":0.0},""" + + """[{"date":10000,"sgv":130,"delta":10,"direction":"Flat","noise":4.5,"units_hint":"mmol","iob":5.2,"tbr":80,"cob":10.7},""" + """{"date":-290000,"sgv":120,"delta":10,"direction":"Flat","noise":4.5}]""", gp.onSgv(mock(), createUri(mapOf("count" to "2", "brief_mode" to "true")), null)) verify(loopHub, times(2)).getGlucoseValues( From 56937dc7045ba71bd6eab4ae16777779c2fed1a8 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 1 Dec 2023 14:13:19 +0100 Subject: [PATCH 33/70] gradle 8.2.0 --- app/build.gradle.kts | 1 + build.gradle.kts | 2 +- buildSrc/build.gradle.kts | 4 ++-- gradle.properties | 1 - wear/build.gradle.kts | 3 +++ 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f6683654a..8db9f1ace1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -153,6 +153,7 @@ android { //Deleting it causes a binding error buildFeatures { dataBinding = true + buildConfig = true } } diff --git a/build.gradle.kts b/build.gradle.kts index 04b3e46ec4..e3e0426ab5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:8.1.4") + classpath("com.android.tools.build:gradle:8.2.0") classpath("com.google.gms:google-services:4.4.0") classpath("com.google.firebase:firebase-crashlytics-gradle:2.9.9") diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 7cd2f0a647..223da698c9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,7 +1,7 @@ object KtsBuildVersions { - const val gradle = "8.1.3" - const val kotlin = "1.9.0" + const val gradle = "8.2.0" + const val kotlin = "1.9.10" } plugins { diff --git a/gradle.properties b/gradle.properties index 743829c702..dc962a673a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -32,5 +32,4 @@ android.nonTransitiveRClass=true # null: KtCallExpression # https://youtrack.jetbrains.com/issue/KT-58027 kapt.use.jvm.ir=false -android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=true diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index ad13b8e525..ce0b3e4539 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -86,6 +86,9 @@ android { versionName = Versions.appVersion + "-aapsclient2" } } + buildFeatures { + buildConfig = true + } } allprojects { From 52335bc7daa58192786d2c8a4171077f62a0bba4 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Wed, 15 Nov 2023 04:56:13 +0100 Subject: [PATCH 34/70] Auto-add battery changes to careportal --- .../pump/MedtronicPumpHistoryDecoder.kt | 4 +- .../medtronic/data/MedtronicHistoryData.kt | 41 +++++++++++++++++++ .../pump/medtronic/util/MedtronicConst.kt | 1 + 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt index 7ea2c36e19..782599ff5a 100644 --- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt +++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt @@ -297,7 +297,9 @@ class MedtronicPumpHistoryDecoder @Inject constructor( } private fun decodeBatteryActivity(entry: PumpHistoryEntry) { - entry.displayableValue = if (entry.head[0] == 0.toByte()) "Battery Removed" else "Battery Replaced" + val isRemoved = entry.head[0] == 0.toByte() + entry.addDecodedData("isRemoved", isRemoved) + entry.displayableValue = if (isRemoved) "Battery Removed" else "Battery Replaced" } private fun decodeBasalProfileStart(entry: PumpHistoryEntry): RecordDecodeStatus { diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt index 27d052bcca..4ab8cb87bf 100644 --- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt +++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt @@ -347,6 +347,18 @@ class MedtronicHistoryData @Inject constructor( } } + // BatteryChange + val batteryChangeRecords: MutableList = getFilteredItems(PumpHistoryEntryType.BatteryChange) + aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: BatteryChange [count=%d, items=%s]", batteryChangeRecords.size, gson.toJson(batteryChangeRecords))) + if (isCollectionNotEmpty(batteryChangeRecords)) { + try { + processBatteryChange(batteryChangeRecords) + } catch (ex: Exception) { + aapsLogger.error(LTag.PUMP, "ProcessHistoryData: Error processing BatteryChange entries: " + ex.message, ex) + throw ex + } + } + // TDD val tdds: MutableList = getFilteredItems(setOf(PumpHistoryEntryType.EndResultTotals, getTDDType())) aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: TDD [count=%d, items=%s]", tdds.size, gson.toJson(tdds))) @@ -456,6 +468,35 @@ class MedtronicHistoryData @Inject constructor( } } + private fun processBatteryChange(batteryChangeRecords: List) { + val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -120) + var lastBatteryChangeRecordTime = 0L + var lastBatteryChangeRecord: PumpHistoryEntry? = null + for (batteryChangeRecord in batteryChangeRecords) { + val isRemoved = batteryChangeRecord.getDecodedDataEntry("isRemoved") + + if (isRemoved != null && isRemoved as Boolean) + { + // we're interested in battery replacements, not battery removals + continue + } + + if (batteryChangeRecord.atechDateTime > maxAllowedTimeInPast) { + if (lastBatteryChangeRecordTime < batteryChangeRecord.atechDateTime) { + lastBatteryChangeRecordTime = batteryChangeRecord.atechDateTime + lastBatteryChangeRecord = batteryChangeRecord + } + } + } + if (lastBatteryChangeRecord != null) { + uploadCareportalEventIfFoundInHistory( + lastBatteryChangeRecord, + MedtronicConst.Statistics.LastBatteryChange, + DetailedBolusInfo.EventType.PUMP_BATTERY_CHANGE + ) + } + } + private fun uploadCareportalEventIfFoundInHistory(historyRecord: PumpHistoryEntry, eventSP: String, eventType: DetailedBolusInfo.EventType) { val lastPrimeFromAAPS = sp.getLong(eventSP, 0L) if (historyRecord.atechDateTime != lastPrimeFromAAPS) { diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt index 8d86bce696..03bc600aa6 100644 --- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt +++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/util/MedtronicConst.kt @@ -30,5 +30,6 @@ object MedtronicConst { const val LastPumpHistoryEntry = StatsPrefix + "pump_history_entry" const val LastPrime = StatsPrefix + "last_sent_prime" const val LastRewind = StatsPrefix + "last_sent_rewind" + const val LastBatteryChange = StatsPrefix + "last_sent_battery_change" } } \ No newline at end of file From faeddef68edcab3749c595f3732e1b4de4a54d4d Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Wed, 15 Nov 2023 06:14:05 +0100 Subject: [PATCH 35/70] Add support for SensorStartedAt extra in xDrip+ BG source --- .../aaps/core/interfaces/receivers/Intents.kt | 1 + .../aaps/plugins/source/XdripSourcePlugin.kt | 30 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt index fbcfca67e6..c4d74505a0 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/receivers/Intents.kt @@ -25,6 +25,7 @@ interface Intents { const val EXTRA_BG_SLOPE = "com.eveningoutpost.dexdrip.Extras.BgSlope" const val EXTRA_BG_SLOPE_NAME = "com.eveningoutpost.dexdrip.Extras.BgSlopeName" const val EXTRA_SENSOR_BATTERY = "com.eveningoutpost.dexdrip.Extras.SensorBattery" + const val EXTRA_SENSOR_STARTED_AT = "com.eveningoutpost.dexdrip.Extras.SensorStartedAt" const val EXTRA_TIMESTAMP = "com.eveningoutpost.dexdrip.Extras.Time" const val EXTRA_RAW = "com.eveningoutpost.dexdrip.Extras.Raw" const val XDRIP_DATA_SOURCE_DESCRIPTION = "com.eveningoutpost.dexdrip.Extras.SourceDesc" diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt index 7bcdddc2d0..1942e424be 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt @@ -5,6 +5,7 @@ import androidx.work.WorkerParameters import androidx.work.workDataOf import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag +import app.aaps.core.interfaces.logging.UserEntryLogger import app.aaps.core.interfaces.plugin.PluginBase import app.aaps.core.interfaces.plugin.PluginDescription import app.aaps.core.interfaces.plugin.PluginType @@ -12,9 +13,14 @@ import app.aaps.core.interfaces.receivers.Intents import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.source.BgSource import app.aaps.core.interfaces.source.XDripSource +import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.interfaces.utils.T import app.aaps.core.main.utils.worker.LoggingWorker import app.aaps.core.utils.receivers.DataWorkerStorage import app.aaps.database.entities.GlucoseValue +import app.aaps.database.entities.UserEntry.Action +import app.aaps.database.entities.UserEntry.Sources +import app.aaps.database.entities.ValueWithUnit import app.aaps.database.impl.AppRepository import app.aaps.database.impl.transactions.CgmSourceTransaction import app.aaps.database.transactions.TransactionGlucoseValue @@ -22,6 +28,7 @@ import dagger.android.HasAndroidInjector import kotlinx.coroutines.Dispatchers import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.abs import kotlin.math.round @Singleton @@ -63,8 +70,10 @@ class XdripSourcePlugin @Inject constructor( ) : LoggingWorker(context, params, Dispatchers.IO) { @Inject lateinit var xdripSourcePlugin: XdripSourcePlugin + @Inject lateinit var dateUtil: DateUtil @Inject lateinit var repository: AppRepository @Inject lateinit var dataWorkerStorage: DataWorkerStorage + @Inject lateinit var uel: UserEntryLogger override suspend fun doWorkAndLog(): Result { var ret = Result.success() @@ -86,17 +95,32 @@ class XdripSourcePlugin @Inject constructor( ?: "" ) ) - repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), null)) + val now = dateUtil.now() + var sensorStartTime: Long? = bundle.getLong(Intents.EXTRA_SENSOR_STARTED_AT, 0) + // check start time validity + sensorStartTime?.let { + if (abs(it - now) > T.months(1).msecs() || it > now) sensorStartTime = null + } + repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), sensorStartTime)) .doOnError { aapsLogger.error(LTag.DATABASE, "Error while saving values from Xdrip", it) ret = Result.failure(workDataOf("Error" to it.toString())) } .blockingGet() - .also { savedValues -> - savedValues.all().forEach { + .also { result -> + result.all().forEach { xdripSourcePlugin.detectSource(it) aapsLogger.debug(LTag.DATABASE, "Inserted bg $it") } + result.sensorInsertionsInserted.forEach { + uel.log( + Action.CAREPORTAL, + Sources.Xdrip, + ValueWithUnit.Timestamp(it.timestamp), + ValueWithUnit.TherapyEventType(it.type) + ) + aapsLogger.debug(LTag.DATABASE, "Inserted sensor insertion $it") + } } xdripSourcePlugin.sensorBatteryLevel = bundle.getInt(Intents.EXTRA_SENSOR_BATTERY, -1) return ret From 13f06ef110a2112964d573808c4763007197fe17 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Fri, 1 Dec 2023 18:32:36 +0100 Subject: [PATCH 36/70] Add a setting for sensor change upload to xDrip BG source --- core/utils/src/main/res/values/keys.xml | 1 + .../aaps/plugins/source/XdripSourcePlugin.kt | 10 ++++++-- .../source/src/main/res/values/strings.xml | 5 ++-- .../source/src/main/res/xml/pref_dexcom.xml | 4 ++-- .../source/src/main/res/xml/pref_xdrip.xml | 23 +++++++++++++++++++ 5 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 plugins/source/src/main/res/xml/pref_xdrip.xml diff --git a/core/utils/src/main/res/values/keys.xml b/core/utils/src/main/res/values/keys.xml index 8493fae85b..1dc4a7f260 100644 --- a/core/utils/src/main/res/values/keys.xml +++ b/core/utils/src/main/res/values/keys.xml @@ -50,6 +50,7 @@ insight_local_settings data_choices_settings dexcom_settings + xdrip_settings active_pump_change_timestamp active_pump_type active_pump_serial_number diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt index 1942e424be..fee5212076 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt @@ -11,6 +11,7 @@ import app.aaps.core.interfaces.plugin.PluginDescription import app.aaps.core.interfaces.plugin.PluginType import app.aaps.core.interfaces.receivers.Intents import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.source.BgSource import app.aaps.core.interfaces.source.XDripSource import app.aaps.core.interfaces.utils.DateUtil @@ -41,7 +42,7 @@ class XdripSourcePlugin @Inject constructor( .mainType(PluginType.BGSOURCE) .fragmentClass(BGSourceFragment::class.java.name) .pluginIcon((app.aaps.core.main.R.drawable.ic_blooddrop_48)) - .preferencesId(R.xml.pref_bgsource) + .preferencesId(R.xml.pref_xdrip) .pluginName(R.string.source_xdrip) .description(R.string.description_source_xdrip), aapsLogger, rh, injector @@ -70,6 +71,7 @@ class XdripSourcePlugin @Inject constructor( ) : LoggingWorker(context, params, Dispatchers.IO) { @Inject lateinit var xdripSourcePlugin: XdripSourcePlugin + @Inject lateinit var sp: SP @Inject lateinit var dateUtil: DateUtil @Inject lateinit var repository: AppRepository @Inject lateinit var dataWorkerStorage: DataWorkerStorage @@ -96,7 +98,11 @@ class XdripSourcePlugin @Inject constructor( ) ) val now = dateUtil.now() - var sensorStartTime: Long? = bundle.getLong(Intents.EXTRA_SENSOR_STARTED_AT, 0) + var sensorStartTime: Long? = if (sp.getBoolean(R.string.key_xdrip_log_ns_sensor_change, false)) { + bundle.getLong(Intents.EXTRA_SENSOR_STARTED_AT, 0) + } else { + null + } // check start time validity sensorStartTime?.let { if (abs(it - now) > T.months(1).msecs() || it > now) sensorStartTime = null diff --git a/plugins/source/src/main/res/values/strings.xml b/plugins/source/src/main/res/values/strings.xml index 9c2db02d03..d2a3aa7d08 100644 --- a/plugins/source/src/main/res/values/strings.xml +++ b/plugins/source/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ dexcom_lognssensorchange + xdrip_lognssensorchange last_processed_glunovo_timestamp last_processed_intelligo_timestamp @@ -39,8 +40,8 @@ Send BG data to xDrip+ In xDrip+ select 640g/Eversense data source BG upload settings - Log sensor change to NS - Create event \"Sensor Change\" in NS automatically on sensor start + Log sensor change to NS + Create event \"Sensor Change\" in NS automatically on sensor start direction diff --git a/plugins/source/src/main/res/xml/pref_dexcom.xml b/plugins/source/src/main/res/xml/pref_dexcom.xml index f9891d0049..2479f27cd6 100644 --- a/plugins/source/src/main/res/xml/pref_dexcom.xml +++ b/plugins/source/src/main/res/xml/pref_dexcom.xml @@ -15,8 +15,8 @@ + android:summary="@string/log_ns_sensor_change_summary" + android:title="@string/log_ns_sensor_change_title" /> diff --git a/plugins/source/src/main/res/xml/pref_xdrip.xml b/plugins/source/src/main/res/xml/pref_xdrip.xml new file mode 100644 index 0000000000..1eb5069016 --- /dev/null +++ b/plugins/source/src/main/res/xml/pref_xdrip.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + \ No newline at end of file From 00e36392bd8006cd114ac3177ffb05ac29d64142 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Fri, 1 Dec 2023 22:50:05 +0100 Subject: [PATCH 37/70] Auto-add "BG Check" treatment for Contour Link readings --- .../app/aaps/core/interfaces/pump/PumpSync.kt | 21 +++++++++ .../pump/PumpSyncImplementation.kt | 38 ++++++++++++++++ .../pump/MedtronicPumpHistoryDecoder.kt | 15 +++++-- .../medtronic/data/MedtronicHistoryData.kt | 43 ++++++++++++++++++- 4 files changed, 112 insertions(+), 5 deletions(-) diff --git a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt index 5a887617e8..ad30c2b047 100644 --- a/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt +++ b/core/interfaces/src/main/kotlin/app/aaps/core/interfaces/pump/PumpSync.kt @@ -1,5 +1,6 @@ package app.aaps.core.interfaces.pump +import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.profile.Profile import app.aaps.core.interfaces.pump.defs.PumpType import app.aaps.core.interfaces.utils.DateUtil @@ -257,6 +258,26 @@ interface PumpSync { **/ fun insertTherapyEventIfNewWithTimestamp(timestamp: Long, type: DetailedBolusInfo.EventType, note: String? = null, pumpId: Long? = null, pumpType: PumpType, pumpSerial: String): Boolean + /** + * Synchronization of FINGER_STICK_BG_VALUE events + * + * Assuming there will be no clash on timestamp from different pumps + * only timestamp and type is compared + * + * If db record doesn't exist, new record is created. + * If exists, data is ignored + * + * @param timestamp timestamp of event from pump history + * @param glucose glucose value + * @param glucoseUnit glucose unit + * @param note note + * @param pumpId pump id from history if available + * @param pumpType pump type like PumpType.ACCU_CHEK_COMBO + * @param pumpSerial pump serial number + * @return true if new record is created + **/ + fun insertFingerBgIfNewWithTimestamp(timestamp: Long, glucose: Double, glucoseUnit: GlucoseUnit, note: String? = null, pumpId: Long? = null, pumpType: PumpType, pumpSerial: String): Boolean + /** * Create an announcement * diff --git a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt index 1db60cde05..bac571d297 100644 --- a/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt +++ b/implementation/src/main/kotlin/app/aaps/implementation/pump/PumpSyncImplementation.kt @@ -1,5 +1,6 @@ package app.aaps.implementation.pump +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.logging.UserEntryLogger @@ -16,6 +17,7 @@ import app.aaps.core.interfaces.sharedPreferences.SP import app.aaps.core.interfaces.utils.DateUtil import app.aaps.core.interfaces.utils.T import app.aaps.core.main.events.EventNewNotification +import app.aaps.core.main.extensions.fromConstant import app.aaps.core.main.pump.fromDbPumpType import app.aaps.core.main.pump.toDbPumpType import app.aaps.core.main.pump.toDbSource @@ -291,6 +293,42 @@ class PumpSyncImplementation @Inject constructor( } } + override fun insertFingerBgIfNewWithTimestamp(timestamp: Long, glucose: Double, glucoseUnit: GlucoseUnit, note: String?, pumpId: Long?, pumpType: PumpType, pumpSerial: String): Boolean { + if (!confirmActivePump(timestamp, pumpType, pumpSerial)) return false + var type = TherapyEvent.Type.FINGER_STICK_BG_VALUE + val therapyEvent = TherapyEvent( + timestamp = timestamp, + type = type, + duration = 0, + note = note, + enteredBy = "AndroidAPS", + glucose = glucose, + glucoseType = TherapyEvent.MeterType.FINGER, + glucoseUnit = TherapyEvent.GlucoseUnit.fromConstant(glucoseUnit), + interfaceIDs_backing = InterfaceIDs( + pumpId = pumpId, + pumpType = pumpType.toDbPumpType(), + pumpSerial = pumpSerial + ) + ) + uel.log( + action = UserEntry.Action.CAREPORTAL, + source = pumpType.source.toDbSource(), + note = note, + timestamp = timestamp, + ValueWithUnit.Timestamp(timestamp), ValueWithUnit.TherapyEventType(type) + ) + repository.runTransactionForResult(InsertIfNewByTimestampTherapyEventTransaction(therapyEvent)) + .doOnError { + aapsLogger.error(LTag.DATABASE, "Error while saving TherapyEvent", it) + } + .blockingGet() + .also { result -> + result.inserted.forEach { aapsLogger.debug(LTag.DATABASE, "Inserted TherapyEvent $it") } + return result.inserted.size > 0 + } + } + override fun insertAnnouncement(error: String, pumpId: Long?, pumpType: PumpType, pumpSerial: String) { if (!confirmActivePump(dateUtil.now(), pumpType, pumpSerial)) return disposable += repository.runTransaction(InsertTherapyEventAnnouncementTransaction(error, pumpId, pumpType.toDbPumpType(), pumpSerial)) diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt index 782599ff5a..606d1058e3 100644 --- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt +++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/MedtronicPumpHistoryDecoder.kt @@ -153,7 +153,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor( PumpHistoryEntryType.ClearAlarm, PumpHistoryEntryType.ChangeAlarmNotifyMode, PumpHistoryEntryType.EnableDisableRemote, - PumpHistoryEntryType.BGReceived, PumpHistoryEntryType.SensorAlert, PumpHistoryEntryType.ChangeTimeFormat, PumpHistoryEntryType.ChangeReservoirWarningTime, @@ -188,7 +187,6 @@ class MedtronicPumpHistoryDecoder @Inject constructor( PumpHistoryEntryType.ChangeWatchdogEnable, PumpHistoryEntryType.ChangeOtherDeviceID, PumpHistoryEntryType.ReadOtherDevicesIDs, - PumpHistoryEntryType.BGReceived512, PumpHistoryEntryType.SensorStatus, PumpHistoryEntryType.ReadCaptureEventEnabled, PumpHistoryEntryType.ChangeCaptureEventEnable, @@ -206,6 +204,12 @@ class MedtronicPumpHistoryDecoder @Inject constructor( PumpHistoryEntryType.UnabsorbedInsulin, PumpHistoryEntryType.UnabsorbedInsulin512 -> RecordDecodeStatus.Ignored + PumpHistoryEntryType.BGReceived, + PumpHistoryEntryType.BGReceived512 -> { + decodeBgReceived(entry) + RecordDecodeStatus.OK + } + PumpHistoryEntryType.DailyTotals522, PumpHistoryEntryType.DailyTotals523, PumpHistoryEntryType.DailyTotals515, @@ -409,8 +413,11 @@ class MedtronicPumpHistoryDecoder @Inject constructor( } private fun decodeBgReceived(entry: PumpHistoryEntry) { - entry.addDecodedData("amount", (ByteUtil.asUINT8(entry.getRawDataByIndex(0)) shl 3) + (ByteUtil.asUINT8(entry.getRawDataByIndex(3)) shr 5)) - entry.addDecodedData("meter", ByteUtil.substring(entry.rawData, 6, 3)) // index moved from 1 -> 0 + val glucoseMgdl = (ByteUtil.asUINT8(entry.head[0]) shl 3) + (ByteUtil.asUINT8(entry.datetime[2]) shr 5) + val meterSerial = ByteUtil.shortHexStringWithoutSpaces(entry.body) + entry.addDecodedData("GlucoseMgdl", glucoseMgdl) + entry.addDecodedData("MeterSerial", meterSerial) + entry.displayableValue = String.format("Glucose: %d mg/dl, Meter Serial: %s", glucoseMgdl, meterSerial) } private fun decodeCalBGForPH(entry: PumpHistoryEntry) { diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt index 4ab8cb87bf..b8856d1a1f 100644 --- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt +++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt @@ -4,6 +4,7 @@ import app.aaps.core.interfaces.logging.AAPSLogger import app.aaps.core.interfaces.logging.LTag import app.aaps.core.interfaces.notifications.Notification import app.aaps.core.interfaces.plugin.ActivePlugin +import app.aaps.core.interfaces.profile.ProfileUtil import app.aaps.core.interfaces.pump.DetailedBolusInfo import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.pump.defs.PumpType @@ -67,7 +68,8 @@ class MedtronicHistoryData @Inject constructor( val medtronicPumpStatus: MedtronicPumpStatus, private val pumpSync: PumpSync, private val pumpSyncStorage: PumpSyncStorage, - private val uiInteraction: UiInteraction + private val uiInteraction: UiInteraction, + private val profileUtil: ProfileUtil ) { val allHistory: MutableList = mutableListOf() @@ -322,6 +324,17 @@ class MedtronicHistoryData @Inject constructor( * Process History Data: Boluses(Treatments), TDD, TBRs, Suspend-Resume (or other pump stops: battery, prime) */ fun processNewHistoryData() { + // Finger BG (for adding entry to careportal) + val bgRecords: MutableList = getFilteredItems(setOf(PumpHistoryEntryType.BGReceived, PumpHistoryEntryType.BGReceived512)) + aapsLogger.debug(LTag.PUMP, String.format(Locale.ENGLISH, "ProcessHistoryData: BGReceived [count=%d, items=%s]", bgRecords.size, gson.toJson(bgRecords))) + if (isCollectionNotEmpty(bgRecords)) { + try { + processBgReceived(bgRecords) + } catch (ex: Exception) { + aapsLogger.error(LTag.PUMP, "ProcessHistoryData: Error processing BGReceived entries: " + ex.message, ex) + throw ex + } + } // Prime (for resetting autosense) val primeRecords: MutableList = getFilteredItems(PumpHistoryEntryType.Prime) @@ -419,6 +432,34 @@ class MedtronicHistoryData @Inject constructor( } } + private fun processBgReceived(bgRecords: List) { + for (bgRecord in bgRecords) { + val glucoseMgdl = bgRecord.getDecodedDataEntry("GlucoseMgdl") + if (glucoseMgdl == null || glucoseMgdl as Int == 0) { + continue + } + + val glucose = profileUtil.fromMgdlToUnits(glucoseMgdl.toDouble()) + val glucoseUnit = profileUtil.units + + val result = pumpSync.insertFingerBgIfNewWithTimestamp( + DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime), + glucose, glucoseUnit, null, + bgRecord.pumpId, + medtronicPumpStatus.pumpType, + medtronicPumpStatus.serialNumber + ) + + aapsLogger.debug( + LTag.PUMP, String.format( + Locale.ROOT, "insertFingerBgIfNewWithTimestamp [date=%d, glucose=%f, glucoseUnit=%s, pumpId=%d, pumpSerial=%s] - Result: %b", + bgRecord.atechDateTime, glucose, glucoseUnit, bgRecord.pumpId, + medtronicPumpStatus.serialNumber, result + ) + ) + } + } + private fun processPrime(primeRecords: List) { val maxAllowedTimeInPast = DateTimeUtil.getATDWithAddedMinutes(GregorianCalendar(), -30) var lastPrimeRecordTime = 0L From f98d310a8e2ecfd5957a2bbd2a8d60eddd8bcd48 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Sat, 2 Dec 2023 01:19:38 +0100 Subject: [PATCH 38/70] Fix missing dependency in tests --- .../plugins/pump/medtronic/MedtronicTestBase.kt | 8 +++----- .../pump/medtronic/comm/MedtronicHistoryDataUTest.kt | 2 +- .../pump/medtronic/data/MedtronicHistoryDataUTest.kt | 4 ++-- .../kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt index 03cfe9d431..e4b074dbaa 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt @@ -4,7 +4,7 @@ import app.aaps.core.interfaces.plugin.ActivePlugin import app.aaps.core.interfaces.pump.PumpSync import app.aaps.core.interfaces.resources.ResourceHelper import app.aaps.core.interfaces.sharedPreferences.SP -import app.aaps.shared.tests.TestBase +import app.aaps.shared.tests.TestBaseWithProfile import dagger.android.AndroidInjector import dagger.android.HasAndroidInjector import info.nightscout.androidaps.plugins.pump.common.hw.rileylink.RileyLinkUtil @@ -16,15 +16,13 @@ import info.nightscout.pump.common.sync.PumpSyncStorage import org.mockito.Answers import org.mockito.Mock -open class MedtronicTestBase : TestBase() { +open class MedtronicTestBase : TestBaseWithProfile() { var rileyLinkUtil = RileyLinkUtil() @Mock lateinit var pumpSync: PumpSync @Mock lateinit var pumpSyncStorage: PumpSyncStorage - @Mock(answer = Answers.RETURNS_DEEP_STUBS) lateinit var activePlugin: ActivePlugin - @Mock lateinit var sp: SP - @Mock lateinit var rh: ResourceHelper + @Mock(answer = Answers.RETURNS_DEEP_STUBS) override lateinit var activePlugin: ActivePlugin lateinit var medtronicUtil: MedtronicUtil lateinit var decoder: MedtronicPumpHistoryDecoder diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt index 70bcd924e4..13a53c78f7 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/MedtronicHistoryDataUTest.kt @@ -40,7 +40,7 @@ import org.mockito.Mock decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) medtronicHistoryData = MedtronicHistoryData( packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, - medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction + medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil ) diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt index d36c40d1ba..d282e82418 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt @@ -32,7 +32,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() { val unitToTest = MedtronicHistoryData( packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, - medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction + medtronicUtil, decoder, medtronicPumpStatus, pumpSync, pumpSyncStorage, uiInteraction, profileUtil ) val gson = Gson() @@ -75,7 +75,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() { medtronicUtil, decoder, medtronicPumpStatus, pumpSync, - pumpSyncStorage, uiInteraction + pumpSyncStorage, uiInteraction, profileUtil ) val gson = Gson() diff --git a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt index 3a170fb5f2..42997cc0ca 100644 --- a/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt +++ b/shared/tests/src/main/kotlin/app/aaps/shared/tests/TestBaseWithProfile.kt @@ -36,7 +36,7 @@ import org.mockito.invocation.InvocationOnMock @Suppress("SpellCheckingInspection") open class TestBaseWithProfile : TestBase() { - @Mock lateinit var activePlugin: ActivePlugin + @Mock open lateinit var activePlugin: ActivePlugin @Mock lateinit var rh: ResourceHelper @Mock lateinit var iobCobCalculator: IobCobCalculator @Mock lateinit var fabricPrivacy: FabricPrivacy From b6af7447e8105b8decd3e00ed8584b8b9edba136 Mon Sep 17 00:00:00 2001 From: ssuppe Date: Sat, 2 Dec 2023 23:09:49 +0000 Subject: [PATCH 39/70] In the profile editor, include profile name in the confirmation dialog (to make it clearer which profile you are deleting). --- plugins/main/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/main/src/main/res/values/strings.xml b/plugins/main/src/main/res/values/strings.xml index a46b55b81e..ced0a2cf96 100644 --- a/plugins/main/src/main/res/values/strings.xml +++ b/plugins/main/src/main/res/values/strings.xml @@ -149,7 +149,7 @@ add new to list Do you want to switch profile and discard changes made to current profile? Save or reset current changes first - Delete current profile? + Delete profile \"%1$s\"? Units: Missing profile name Error in IC values From 2a1ddc956f98e2767e8114f4868c319e6e488544 Mon Sep 17 00:00:00 2001 From: ssuppe Date: Sat, 2 Dec 2023 23:42:54 +0000 Subject: [PATCH 40/70] Added profile name to the actual code (not just the string). --- .../kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt b/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt index 5eb7f3d41d..0415399cc4 100644 --- a/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt +++ b/plugins/main/src/main/kotlin/app/aaps/plugins/main/profile/ProfileFragment.kt @@ -300,7 +300,7 @@ class ProfileFragment : DaggerFragment() { binding.profileRemove.setOnClickListener { activity?.let { activity -> - OKDialog.showConfirmation(activity, rh.gs(R.string.delete_current_profile), { + OKDialog.showConfirmation(activity, rh.gs(R.string.delete_current_profile, profilePlugin.currentProfile()?.name), { uel.log( UserEntry.Action.PROFILE_REMOVED, UserEntry.Sources.LocalProfile, ValueWithUnit.SimpleString( profilePlugin.currentProfile()?.name From 6de92da1da7547a85d35f33b4eb2eea6be9ac135 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 08:01:17 +0000 Subject: [PATCH 41/70] chore(deps): bump org.jlleitschuh.gradle.ktlint from 11.6.1 to 12.0.2 Bumps org.jlleitschuh.gradle.ktlint from 11.6.1 to 12.0.2. --- updated-dependencies: - dependency-name: org.jlleitschuh.gradle.ktlint dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index e3e0426ab5..e2bf637c52 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ buildscript { } plugins { - id("org.jlleitschuh.gradle.ktlint") version "11.6.1" + id("org.jlleitschuh.gradle.ktlint") version "12.0.2" } allprojects { From 4a38a5b2190757a7d0e535c95849f9a412d0eab8 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Tue, 5 Dec 2023 01:44:46 +0100 Subject: [PATCH 42/70] Add unit tests --- .../medtronic/data/MedtronicHistoryData.kt | 2 +- .../pump/medtronic/MedtronicTestBase.kt | 18 +++++ .../history/pump/PumpHistoryEntryUTest.kt | 35 +++++++++ .../data/MedtronicHistoryDataUTest.kt | 73 +++++++++++++++++++ 4 files changed, 127 insertions(+), 1 deletion(-) diff --git a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt index b8856d1a1f..03bf6c49c3 100644 --- a/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt +++ b/pump/medtronic/src/main/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryData.kt @@ -432,7 +432,7 @@ class MedtronicHistoryData @Inject constructor( } } - private fun processBgReceived(bgRecords: List) { + fun processBgReceived(bgRecords: List) { for (bgRecord in bgRecords) { val glucoseMgdl = bgRecord.getDecodedDataEntry("GlucoseMgdl") if (glucoseMgdl == null || glucoseMgdl as Int == 0) { diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt index e4b074dbaa..0f25f4cd98 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/MedtronicTestBase.kt @@ -51,6 +51,24 @@ open class MedtronicTestBase : TestBaseWithProfile() { } + fun getPumpHistoryEntryFromData(vararg elements: Int): PumpHistoryEntry { + val data: MutableList = ArrayList() + for (item in elements) { + var b = if (item > 128) item - 256 else item + data.add(b.toByte()); + } + + val entryType = PumpHistoryEntryType.getByCode(data[0]) + + val phe = PumpHistoryEntry() + phe.setEntryType(medtronicUtil.medtronicPumpModel, entryType) + phe.setData(data, false) + + decoder.decodeRecord(phe) + + return phe + } + private fun preProcessTBRs(tbrsInput: MutableList): MutableList { val tbrs: MutableList = mutableListOf() val map: MutableMap = HashMap() diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt index 8b07c7dc2c..2743439726 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/comm/history/pump/PumpHistoryEntryUTest.kt @@ -1,8 +1,16 @@ package info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump +import app.aaps.core.interfaces.ui.UiInteraction import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase +import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.MedtronicPumpHistoryDecoder +import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicDeviceType +import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus +import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil 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.`when` /** * Created by andy on 4/9/19. @@ -10,6 +18,16 @@ import org.junit.jupiter.api.Test */ class PumpHistoryEntryUTest : MedtronicTestBase() { + @Mock lateinit var medtronicPumpStatus: MedtronicPumpStatus + @Mock lateinit var uiInteraction: UiInteraction + + @BeforeEach + fun setUp() { + medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction) + `when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel) + decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) + } + @Test fun checkIsAfter() { val dateObject = 20191010000000L @@ -18,4 +36,21 @@ class PumpHistoryEntryUTest : MedtronicTestBase() { phe.atechDateTime = dateObject assertThat(phe.isAfter(queryObject)).isTrue() } + + @Test + fun decodeBgReceived() { + val bgRecord = getPumpHistoryEntryFromData( + // head + 0x39, 0x15, + // datetime (combined with glucose in mg/dl) + 0xC2, 0x25, 0xF3, 0x61, 0x17, + // serial number + 0x12, 0x34, 0x56 + ) + val expectedGlucoseMgdl = 175 + val expectedMeterSerial = "123456" + + assertThat(bgRecord.getDecodedDataEntry("GlucoseMgdl")).isEqualTo(expectedGlucoseMgdl) + assertThat(bgRecord.getDecodedDataEntry("MeterSerial")).isEqualTo(expectedMeterSerial) + } } diff --git a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt index d282e82418..2a91afaba4 100644 --- a/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt +++ b/pump/medtronic/src/test/java/info/nightscout/androidaps/plugins/pump/medtronic/data/MedtronicHistoryDataUTest.kt @@ -1,18 +1,24 @@ package info.nightscout.androidaps.plugins.pump.medtronic.data +import app.aaps.core.interfaces.db.GlucoseUnit import app.aaps.core.interfaces.ui.UiInteraction +import app.aaps.core.utils.DateTimeUtil import com.google.gson.Gson import com.google.gson.internal.LinkedTreeMap import com.google.gson.reflect.TypeToken import info.nightscout.androidaps.plugins.pump.medtronic.MedtronicTestBase import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.MedtronicPumpHistoryDecoder import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntry +import info.nightscout.androidaps.plugins.pump.medtronic.comm.history.pump.PumpHistoryEntryType import info.nightscout.androidaps.plugins.pump.medtronic.data.dto.TempBasalPair +import info.nightscout.androidaps.plugins.pump.medtronic.defs.MedtronicDeviceType import info.nightscout.androidaps.plugins.pump.medtronic.driver.MedtronicPumpStatus import info.nightscout.androidaps.plugins.pump.medtronic.util.MedtronicUtil import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import java.lang.reflect.Type @Suppress("UNCHECKED_CAST") @@ -24,6 +30,7 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() { @BeforeEach fun setUp() { medtronicUtil = MedtronicUtil(aapsLogger, rxBus, rileyLinkUtil, medtronicPumpStatus, uiInteraction) + `when`(medtronicUtil.medtronicPumpModel).thenReturn(MedtronicDeviceType.Medtronic_723_Revel) decoder = MedtronicPumpHistoryDecoder(aapsLogger, medtronicUtil) } @@ -110,4 +117,70 @@ class MedtronicHistoryDataUTest : MedtronicTestBase() { } + @Test + fun processBgReceived_WithMgdl() { + + val unitToTest = MedtronicHistoryData( + packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, + medtronicUtil, decoder, + medtronicPumpStatus, + pumpSync, + pumpSyncStorage, uiInteraction, profileUtil + ) + + val glucoseMgdl = 175 + + `when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn(GlucoseUnit.MGDL.asText) + + val bgRecord = PumpHistoryEntry() + bgRecord.setEntryType(medtronicUtil.medtronicPumpModel, PumpHistoryEntryType.BGReceived) + bgRecord.addDecodedData("GlucoseMgdl", glucoseMgdl) + bgRecord.addDecodedData("MeterSerial", "123456") + + unitToTest.processBgReceived(listOf(bgRecord)) + + verify(pumpSync).insertFingerBgIfNewWithTimestamp( + DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime), + glucoseMgdl.toDouble(), + GlucoseUnit.MGDL, null, + bgRecord.pumpId, + medtronicPumpStatus.pumpType, + medtronicPumpStatus.serialNumber + ) + + } + + @Test + fun processBgReceived_WithMmol() { + + val unitToTest = MedtronicHistoryData( + packetInjector, aapsLogger, sp, rh, rxBus, activePlugin, + medtronicUtil, decoder, + medtronicPumpStatus, + pumpSync, + pumpSyncStorage, uiInteraction, profileUtil + ) + val glucoseMgdl = 180 + val glucoseMmol = 10.0 + + `when`(sp.getString(app.aaps.core.utils.R.string.key_units, GlucoseUnit.MGDL.asText)).thenReturn(GlucoseUnit.MMOL.asText) + + val bgRecord = PumpHistoryEntry() + bgRecord.setEntryType(medtronicUtil.medtronicPumpModel, PumpHistoryEntryType.BGReceived) + bgRecord.addDecodedData("GlucoseMgdl", glucoseMgdl) + bgRecord.addDecodedData("MeterSerial", "123456") + + unitToTest.processBgReceived(listOf(bgRecord)) + + verify(pumpSync).insertFingerBgIfNewWithTimestamp( + DateTimeUtil.toMillisFromATD(bgRecord.atechDateTime), + glucoseMmol, + GlucoseUnit.MMOL, null, + bgRecord.pumpId, + medtronicPumpStatus.pumpType, + medtronicPumpStatus.serialNumber + ) + + } + } From 35b3ce0300e498d71673d7ce5b80fcda58be6e8b Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Tue, 5 Dec 2023 15:37:30 +0100 Subject: [PATCH 43/70] pull upstream/dev --- .../src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt | 1 - .../test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index 617e84cdc1..44b080b661 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -381,7 +381,6 @@ class GarminPlugin @Inject constructor( /** Returns glucose values in Nightscout/Xdrip format. */ @VisibleForTesting - @Suppress("UNUSED_PARAMETER") fun onSgv(uri: URI): CharSequence { val count = getQueryParameter(uri,"count", 24L) .toInt().coerceAtMost(1000).coerceAtLeast(1) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt index 8d4d6b2cc2..e1e9fa89a9 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminPluginTest.kt @@ -14,7 +14,6 @@ import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.RepeatedTest import org.junit.jupiter.api.Test import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyBoolean From 65b34599d01c944bc092bd4ca5771e365294e9f3 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Tue, 5 Dec 2023 23:25:50 +0100 Subject: [PATCH 44/70] Add unit tests --- plugins/source/build.gradle.kts | 2 + .../aaps/plugins/source/XdripSourcePlugin.kt | 26 +++-- .../plugins/source/XdripSourcePluginTest.kt | 94 ++++++++++++++++++- 3 files changed, 110 insertions(+), 12 deletions(-) diff --git a/plugins/source/build.gradle.kts b/plugins/source/build.gradle.kts index df0c6cf34a..4b0ac810c4 100644 --- a/plugins/source/build.gradle.kts +++ b/plugins/source/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { implementation(project(":core:ui")) implementation(project(":core:utils")) + testImplementation(Libs.AndroidX.Work.testing) + testImplementation(project(":shared:tests")) kapt(Libs.Dagger.compiler) diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt index fee5212076..dcb91782f7 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt @@ -1,6 +1,7 @@ package app.aaps.plugins.source import android.content.Context +import android.os.Bundle import androidx.work.WorkerParameters import androidx.work.workDataOf import app.aaps.core.interfaces.logging.AAPSLogger @@ -77,6 +78,20 @@ class XdripSourcePlugin @Inject constructor( @Inject lateinit var dataWorkerStorage: DataWorkerStorage @Inject lateinit var uel: UserEntryLogger + fun getSensorStartTime(bundle: Bundle): Long? { + val now = dateUtil.now() + var sensorStartTime: Long? = if (sp.getBoolean(R.string.key_xdrip_log_ns_sensor_change, false)) { + bundle.getLong(Intents.EXTRA_SENSOR_STARTED_AT, 0) + } else { + null + } + // check start time validity + sensorStartTime?.let { + if (abs(it - now) > T.months(1).msecs() || it > now) sensorStartTime = null + } + return sensorStartTime + } + override suspend fun doWorkAndLog(): Result { var ret = Result.success() @@ -97,16 +112,7 @@ class XdripSourcePlugin @Inject constructor( ?: "" ) ) - val now = dateUtil.now() - var sensorStartTime: Long? = if (sp.getBoolean(R.string.key_xdrip_log_ns_sensor_change, false)) { - bundle.getLong(Intents.EXTRA_SENSOR_STARTED_AT, 0) - } else { - null - } - // check start time validity - sensorStartTime?.let { - if (abs(it - now) > T.months(1).msecs() || it > now) sensorStartTime = null - } + val sensorStartTime = getSensorStartTime(bundle) repository.runTransactionForResult(CgmSourceTransaction(glucoseValues, emptyList(), sensorStartTime)) .doOnError { aapsLogger.error(LTag.DATABASE, "Error while saving values from Xdrip", it) diff --git a/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt b/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt index 3e645fb3e9..360a837549 100644 --- a/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt +++ b/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt @@ -1,25 +1,115 @@ package app.aaps.plugins.source +import android.content.Context +import android.os.Bundle +import androidx.work.WorkerFactory +import androidx.work.WorkerParameters +import androidx.work.testing.TestListenableWorkerBuilder +import app.aaps.core.interfaces.receivers.Intents import app.aaps.core.interfaces.resources.ResourceHelper +import app.aaps.core.interfaces.utils.DateUtil +import app.aaps.core.interfaces.utils.T +import app.aaps.core.interfaces.sharedPreferences.SP +import app.aaps.core.utils.receivers.DataWorkerStorage +import app.aaps.shared.impl.utils.DateUtilImpl +import app.aaps.shared.tests.BundleMock import app.aaps.shared.tests.TestBase import com.google.common.truth.Truth.assertThat import dagger.android.AndroidInjector +import dagger.android.HasAndroidInjector import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.Mock +import org.mockito.Mockito.`when` class XdripSourcePluginTest : TestBase() { - private lateinit var xdripSourcePlugin: XdripSourcePlugin + abstract class ContextWithInjector : Context(), HasAndroidInjector + private lateinit var xdripSourcePlugin: XdripSourcePlugin + private lateinit var dateUtil: DateUtil + private lateinit var dataWorkerStorage: DataWorkerStorage + + private val injector = HasAndroidInjector { + AndroidInjector { + if (it is XdripSourcePlugin.XdripSourceWorker) { + it.dataWorkerStorage = dataWorkerStorage + it.dateUtil = dateUtil + it.sp = sp + } + } + } + + @Mock lateinit var sp: SP @Mock lateinit var rh: ResourceHelper + @Mock lateinit var context: ContextWithInjector @BeforeEach fun setup() { - xdripSourcePlugin = XdripSourcePlugin({ AndroidInjector { } }, rh, aapsLogger) + `when`(context.applicationContext).thenReturn(context) + `when`(context.androidInjector()).thenReturn(injector.androidInjector()) + xdripSourcePlugin = XdripSourcePlugin(injector, rh, aapsLogger) + dateUtil = DateUtilImpl(context) + dataWorkerStorage = DataWorkerStorage(context) + } + + private fun prepareWorker( + sensorStartTime: Long? = dateUtil.now(), + logNsSensorChange: Boolean = true, + ): Pair { + val bundle = BundleMock.mock() + sensorStartTime?.let { bundle.putLong(Intents.EXTRA_SENSOR_STARTED_AT, sensorStartTime) } + `when`(sp.getBoolean(R.string.key_xdrip_log_ns_sensor_change, false)).thenReturn(logNsSensorChange) + + lateinit var worker: XdripSourcePlugin.XdripSourceWorker + TestListenableWorkerBuilder(context) + .setWorkerFactory(object: WorkerFactory() { + override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): XdripSourcePlugin.XdripSourceWorker { + worker = XdripSourcePlugin.XdripSourceWorker(context, workerParameters) + return worker + } + }) + .setInputData(dataWorkerStorage.storeInputData(bundle, Intents.ACTION_NEW_BG_ESTIMATE)).build() + + return Pair(bundle, worker) } @Test fun advancedFilteringSupported() { assertThat(xdripSourcePlugin.advancedFilteringSupported()).isFalse() } + + @Test fun getSensorStartTime_withoutValue_returnsNull() { + val (bundle, worker) = prepareWorker(sensorStartTime = null) + + val result = worker.getSensorStartTime(bundle) + + assertThat(result).isNull() + } + + @Test fun getSensorStartTime_withSettingDisabled_returnsNull() { + val sensorStartTime = dateUtil.now() + val (bundle, worker) = prepareWorker(sensorStartTime = sensorStartTime, logNsSensorChange = false) + + val result = worker.getSensorStartTime(bundle) + + assertThat(result).isNull() + } + + @Test fun getSensorStartTime_withRecentValue_returnsStartTime() { + val sensorStartTime = dateUtil.now() + val (bundle, worker) = prepareWorker(sensorStartTime = sensorStartTime) + + val result = worker.getSensorStartTime(bundle) + + assertThat(result).isEqualTo(sensorStartTime) + } + + @Test fun getSensorStartTime_withOldValue_returnsNull() { + val sensorStartTime = dateUtil.now() - T.months(2).msecs() + val (bundle, worker) = prepareWorker(sensorStartTime = sensorStartTime) + + val result = worker.getSensorStartTime(bundle) + + assertThat(result).isNull() + } } From 6528f9cdc37a5946622641949545c3a6bd1e7a59 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 6 Dec 2023 09:31:18 +0100 Subject: [PATCH 45/70] WizardDialog: move buttons to bottom --- ui/src/main/res/layout/dialog_wizard.xml | 50 ++++++++++++------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/ui/src/main/res/layout/dialog_wizard.xml b/ui/src/main/res/layout/dialog_wizard.xml index 885d3204f9..abf256d80c 100644 --- a/ui/src/main/res/layout/dialog_wizard.xml +++ b/ui/src/main/res/layout/dialog_wizard.xml @@ -284,31 +284,6 @@ - - - - - - - - + + + + + + + + From 05895b643c7c5efc7df67307cf0da29327fc410c Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 6 Dec 2023 15:35:54 +0100 Subject: [PATCH 46/70] Garmin: libs update --- buildSrc/src/main/kotlin/Libs.kt | 16 +--------------- plugins/sync/build.gradle.kts | 4 ++-- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index db6e48f53a..ecc70d9727 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -200,19 +200,5 @@ object Libs { const val commonCodecs = "commons-codec:commons-codec:1.16.0" const val kulid = "com.github.guepardoapps:kulid:2.0.0.0" const val xstream = "com.thoughtworks.xstream:xstream:1.4.20" - - const val ormLite = "4.46" - - const val junit = "4.13.2" - const val mockito = "5.6.0" - const val dexmaker = "1.2" - const val byteBuddy = "1.12.8" - - const val androidx_junit = "1.1.5" - const val androidx_rules = "1.5.0" - - const val kotlinx_datetime = "0.4.1" - const val kotlinx_serialization = "1.6.0" - - const val caverock_androidsvg = "1.4" + const val connectiqSdk = "com.garmin.connectiq:ciq-companion-app-sdk:2.0.3@aar" } \ No newline at end of file diff --git a/plugins/sync/build.gradle.kts b/plugins/sync/build.gradle.kts index 81f4671523..3261c6db02 100644 --- a/plugins/sync/build.gradle.kts +++ b/plugins/sync/build.gradle.kts @@ -51,8 +51,8 @@ dependencies { api(Libs.Google.Android.PlayServices.wearable) // Garmin - api("com.garmin.connectiq:ciq-companion-app-sdk:2.0.2@aar") - androidTestImplementation("com.garmin.connectiq:ciq-companion-app-sdk:2.0.2@aar") + api(Libs.connectiqSdk) + androidTestImplementation(Libs.connectiqSdk) kapt(Libs.Dagger.compiler) kapt(Libs.Dagger.androidProcessor) From 78f1fe096a514f75ef9f0a1b42e2f29a5cb46c2c Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Wed, 6 Dec 2023 15:51:55 +0100 Subject: [PATCH 47/70] Remove xdrip prefs file --- core/utils/src/main/res/values/keys.xml | 1 - .../aaps/plugins/source/XdripSourcePlugin.kt | 4 ++-- .../source/src/main/res/values/strings.xml | 5 ++-- .../source/src/main/res/xml/pref_dexcom.xml | 4 ++-- .../source/src/main/res/xml/pref_xdrip.xml | 23 ------------------- .../plugins/source/XdripSourcePluginTest.kt | 2 +- 6 files changed, 7 insertions(+), 32 deletions(-) delete mode 100644 plugins/source/src/main/res/xml/pref_xdrip.xml diff --git a/core/utils/src/main/res/values/keys.xml b/core/utils/src/main/res/values/keys.xml index 1dc4a7f260..8493fae85b 100644 --- a/core/utils/src/main/res/values/keys.xml +++ b/core/utils/src/main/res/values/keys.xml @@ -50,7 +50,6 @@ insight_local_settings data_choices_settings dexcom_settings - xdrip_settings active_pump_change_timestamp active_pump_type active_pump_serial_number diff --git a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt index dcb91782f7..0ee8e1d7e8 100644 --- a/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt +++ b/plugins/source/src/main/kotlin/app/aaps/plugins/source/XdripSourcePlugin.kt @@ -43,7 +43,7 @@ class XdripSourcePlugin @Inject constructor( .mainType(PluginType.BGSOURCE) .fragmentClass(BGSourceFragment::class.java.name) .pluginIcon((app.aaps.core.main.R.drawable.ic_blooddrop_48)) - .preferencesId(R.xml.pref_xdrip) + .preferencesId(R.xml.pref_dexcom) .pluginName(R.string.source_xdrip) .description(R.string.description_source_xdrip), aapsLogger, rh, injector @@ -80,7 +80,7 @@ class XdripSourcePlugin @Inject constructor( fun getSensorStartTime(bundle: Bundle): Long? { val now = dateUtil.now() - var sensorStartTime: Long? = if (sp.getBoolean(R.string.key_xdrip_log_ns_sensor_change, false)) { + var sensorStartTime: Long? = if (sp.getBoolean(R.string.key_dexcom_log_ns_sensor_change, false)) { bundle.getLong(Intents.EXTRA_SENSOR_STARTED_AT, 0) } else { null diff --git a/plugins/source/src/main/res/values/strings.xml b/plugins/source/src/main/res/values/strings.xml index d2a3aa7d08..9c2db02d03 100644 --- a/plugins/source/src/main/res/values/strings.xml +++ b/plugins/source/src/main/res/values/strings.xml @@ -2,7 +2,6 @@ dexcom_lognssensorchange - xdrip_lognssensorchange last_processed_glunovo_timestamp last_processed_intelligo_timestamp @@ -40,8 +39,8 @@ Send BG data to xDrip+ In xDrip+ select 640g/Eversense data source BG upload settings - Log sensor change to NS - Create event \"Sensor Change\" in NS automatically on sensor start + Log sensor change to NS + Create event \"Sensor Change\" in NS automatically on sensor start direction diff --git a/plugins/source/src/main/res/xml/pref_dexcom.xml b/plugins/source/src/main/res/xml/pref_dexcom.xml index 2479f27cd6..f9891d0049 100644 --- a/plugins/source/src/main/res/xml/pref_dexcom.xml +++ b/plugins/source/src/main/res/xml/pref_dexcom.xml @@ -15,8 +15,8 @@ + android:summary="@string/dexcom_log_ns_sensor_change_summary" + android:title="@string/dexcom_log_ns_sensor_change_title" /> diff --git a/plugins/source/src/main/res/xml/pref_xdrip.xml b/plugins/source/src/main/res/xml/pref_xdrip.xml deleted file mode 100644 index 1eb5069016..0000000000 --- a/plugins/source/src/main/res/xml/pref_xdrip.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt b/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt index 360a837549..6afe6d546a 100644 --- a/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt +++ b/plugins/source/src/test/kotlin/app/aaps/plugins/source/XdripSourcePluginTest.kt @@ -59,7 +59,7 @@ class XdripSourcePluginTest : TestBase() { ): Pair { val bundle = BundleMock.mock() sensorStartTime?.let { bundle.putLong(Intents.EXTRA_SENSOR_STARTED_AT, sensorStartTime) } - `when`(sp.getBoolean(R.string.key_xdrip_log_ns_sensor_change, false)).thenReturn(logNsSensorChange) + `when`(sp.getBoolean(R.string.key_dexcom_log_ns_sensor_change, false)).thenReturn(logNsSensorChange) lateinit var worker: XdripSourcePlugin.XdripSourceWorker TestListenableWorkerBuilder(context) From bafdaa304d959989322e95f16c46f6220f0e474e Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 6 Dec 2023 18:09:42 +0100 Subject: [PATCH 48/70] joda -> java.time --- .../constraints/versionChecker/AllowedVersions.kt | 7 +++++-- .../constraints/versionChecker/AllowedVersionsTest.kt | 11 ++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt index de331c6bd5..69dc86b752 100644 --- a/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt +++ b/plugins/constraints/src/main/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersions.kt @@ -1,9 +1,11 @@ package app.aaps.plugins.constraints.versionChecker -import org.joda.time.LocalDate import org.json.JSONArray import org.json.JSONException import org.json.JSONObject +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneId class AllowedVersions { @@ -38,7 +40,8 @@ class AllowedVersions { fun endDateToMilliseconds(endDate: String): Long? { try { val dateTime = LocalDate.parse(endDate) - return dateTime.toDate().time + val instant = dateTime.atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant() + return instant.toEpochMilli() } catch (ignored: Exception) { } return null diff --git a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt index 0d8f29b5ad..5e18206329 100644 --- a/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt +++ b/plugins/constraints/src/test/kotlin/app/aaps/plugins/constraints/versionChecker/AllowedVersionsTest.kt @@ -1,11 +1,12 @@ package app.aaps.plugins.constraints.versionChecker import com.google.common.truth.Truth.assertThat -import app.aaps.plugins.constraints.versionChecker.AllowedVersions -import org.joda.time.LocalDate import org.json.JSONArray import org.json.JSONObject import org.junit.jupiter.api.Test +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId class AllowedVersionsTest { @@ -71,10 +72,10 @@ class AllowedVersionsTest { @Test fun endDateToMilliseconds() { val definition = generateSupportedVersions() - val endDate = AllowedVersions().endDateToMilliseconds(AllowedVersions().findByVersion(definition, "2.9.0-beta1")?.getString("endDate") ?: "1000/01/01") - val dateTime = LocalDate(endDate) + val endDate = AllowedVersions().endDateToMilliseconds(AllowedVersions().findByVersion(definition, "2.9.0-beta1")?.getString("endDate") ?: "1000/01/01") ?: 0L + val dateTime = LocalDate.ofInstant(Instant.ofEpochMilli(endDate), ZoneId.systemDefault()) assertThat(dateTime.year).isEqualTo(2021) - assertThat(dateTime.monthOfYear).isEqualTo(11) + assertThat(dateTime.monthValue).isEqualTo(11) assertThat(dateTime.dayOfMonth).isEqualTo(7) assertThat(AllowedVersions().endDateToMilliseconds("abdef")).isNull() From d686e629963c292c86c23703af4d06c28c989e89 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Wed, 6 Dec 2023 20:27:38 +0100 Subject: [PATCH 49/70] Fix double /connect endpoint --- .../main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt index 44b080b661..cc8661828f 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminPlugin.kt @@ -142,7 +142,7 @@ class GarminPlugin @Inject constructor( registerEndpoint("/get", requestHandler(::onGetBloodGlucose)) registerEndpoint("/carbs", requestHandler(::onPostCarbs)) registerEndpoint("/connect", requestHandler(::onConnectPump)) - registerEndpoint("/connect", requestHandler(::onSgv)) + registerEndpoint("/sgv.json", requestHandler(::onSgv)) awaitReady(wait) } } else if (server != null) { From fee3c257399271adea693db403e2b1168b2496b7 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 7 Dec 2023 15:17:32 +0100 Subject: [PATCH 50/70] MidnightUtils -> java.time, tests, javadoc --- .../app/aaps/core/utils/MidnightUtils.kt | 54 ++++++++++++--- .../app/aaps/core/utils/MidnightUtilsTest.kt | 67 +++++++++++++++++++ 2 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt b/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt index 3d36fa494a..dfcfab28f5 100644 --- a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt +++ b/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt @@ -1,22 +1,56 @@ package app.aaps.core.utils -import org.joda.time.DateTime +import java.time.Duration +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime -object MidnightUtils { - /* +/** * Midnight time conversion */ +object MidnightUtils { + + /** + * Actual passed seconds from midnight ignoring DST change + * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) + * + * @return seconds + */ fun secondsFromMidnight(): Int { - val passed = DateTime().millisOfDay.toLong() - return (passed / 1000).toInt() + val nowZoned = ZonedDateTime.now() + val localTime = nowZoned.toLocalTime() + val midnight: Instant = nowZoned.toLocalDate().atStartOfDay(nowZoned.zone).toInstant() + val duration: Duration = Duration.between(midnight, localTime) + return duration.seconds.toInt() } - fun secondsFromMidnight(date: Long): Int { - val passed = DateTime(date).millisOfDay.toLong() - return (passed / 1000).toInt() + /** + * Passed seconds from midnight for specified time ignoring DST change + * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) + * + * @param timestamp time + * @return seconds + */ + fun secondsFromMidnight(timestamp: Long): Int { + val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()) + val localTime = timeZoned.toLocalTime() + val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime() + val duration: Duration = Duration.between(midnight, localTime) + return duration.seconds.toInt() } - fun milliSecFromMidnight(date: Long): Long { - return DateTime(date).millisOfDay.toLong() + /** + * Passed milliseconds from midnight for specified time ignoring DST change + * (thus always having 24 hours in a day, not 23 or 25 in days where DST changes) + * + * @param timestamp time + * @return milliseconds + */ + fun milliSecFromMidnight(timestamp: Long): Long { + val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()) + val localTime = timeZoned.toLocalTime() + val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime() + val duration: Duration = Duration.between(midnight, localTime) + return duration.toMillis() } } \ No newline at end of file diff --git a/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt b/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt new file mode 100644 index 0000000000..46d4b46fae --- /dev/null +++ b/core/utils/src/test/kotlin/app/aaps/core/utils/MidnightUtilsTest.kt @@ -0,0 +1,67 @@ +package app.aaps.core.utils + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.util.TimeZone + +class MidnightUtilsTest { + + @BeforeEach fun setUp() { + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Amsterdam")) + } + + @Test + fun secondsFromMidnight() { + val time = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(time)).isIn(0..24 * 3600) + } + + @Test + fun testSecondsFromMidnight() { + val midnight = LocalDate.now().atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(midnight)).isEqualTo(0) + val oneHourAfter = LocalDateTime.ofInstant(Instant.ofEpochMilli(midnight), ZoneId.systemDefault()).atZone(ZoneId.systemDefault()).plusHours(1).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(oneHourAfter)).isEqualTo(3600) + } + + @Test + fun milliSecFromMidnight() { + val midnight = LocalDate.now().atTime(LocalTime.MIDNIGHT).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(midnight)).isEqualTo(0) + val oneHourAfter = LocalDateTime.ofInstant(Instant.ofEpochMilli(midnight), ZoneId.systemDefault()).atZone(ZoneId.systemDefault()).plusHours(1).toInstant().toEpochMilli() + assertThat(MidnightUtils.milliSecFromMidnight(oneHourAfter)).isEqualTo(3600 * 1000) + } + + @Test fun testDateTimeToDuration() { + val dateTime = ZonedDateTime.of(1991, 8, 13, 23, 5, 1, 0, ZoneId.of("Europe/Amsterdam")).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101) + assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L) + } + + @Test fun testDateTimeToDurationAtDstChange() { + val dateTime = ZonedDateTime.of(2020, 10, 25, 23, 5, 1, 0, ZoneId.of("Europe/Amsterdam")).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101) + assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L) + } + + @Test fun testDateTimeToDurationAtDstReverseChange() { + val dateTime = ZonedDateTime.of(2020, 3, 29, 23, 5, 1, 0, ZoneId.of("Europe/Amsterdam")).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101) + assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L) + } + + @Test fun testDateTimeInOtherZone() { + TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")) + assertThat(ZoneId.systemDefault().id).isEqualTo("America/Los_Angeles") + val dateTime = ZonedDateTime.of(2020, 3, 29, 23, 5, 1, 0, ZoneId.of("America/Los_Angeles")).toInstant().toEpochMilli() + assertThat(MidnightUtils.secondsFromMidnight(dateTime)).isEqualTo(83101) + assertThat(MidnightUtils.milliSecFromMidnight(dateTime)).isEqualTo(83101 * 1000L) + } +} \ No newline at end of file From 6e4c7d9376701ec225ca92d28f3e0edff38ffb75 Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Thu, 7 Dec 2023 20:56:25 +0100 Subject: [PATCH 51/70] Increase Gradle JVM memory from 3 to 4G --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index dc962a673a..8e28702445 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true org.gradle.warning.mode=all -org.gradle.jvmargs=-Xmx3g -XX:+UseParallelGC +org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC android.enableJetifier=false android.useAndroidX=true From ec596c51df70776801f04fad4e54bb172a622ba0 Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Thu, 7 Dec 2023 22:18:47 +0100 Subject: [PATCH 52/70] Fix flaky GarminSimulatorClientTest.receiveMessage --- .../aaps/plugins/sync/garmin/GarminSimulatorClient.kt | 7 ++++--- .../plugins/sync/garmin/GarminSimulatorClientTest.kt | 9 ++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt index 75f8d8e436..01712a8da6 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClient.kt @@ -13,6 +13,7 @@ import java.net.ServerSocket import java.net.Socket import java.net.SocketException import java.time.Duration +import java.time.Instant import java.util.Collections import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -142,10 +143,10 @@ class GarminSimulatorClient( /** Wait for the server to start listing to requests. */ fun awaitReady(wait: Duration): Boolean { - var waitNanos = wait.toNanos() + val waitUntil = Instant.now() + wait readyLock.withLock { - while (!serverSocket.isBound && waitNanos > 0L) { - waitNanos = readyCond.awaitNanos(waitNanos) + while (!serverSocket.isBound && Instant.now() < waitUntil) { + readyCond.await(20, TimeUnit.MILLISECONDS) } } return serverSocket.isBound diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt index 997213304a..d2e318ee45 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt @@ -5,6 +5,7 @@ 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.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.isNull import org.mockito.kotlin.mock @@ -28,18 +29,16 @@ class GarminSimulatorClientTest: TestBase() { fun receiveMessage() { val payload = "foo".toByteArray() assertTrue(client.awaitReady(Duration.ofSeconds(10))) + verify(receiver, timeout(100)).onConnect(client) val port = client.port val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) Socket(ip, port).use { socket -> assertTrue(socket.isConnected) socket.getOutputStream().write(payload) socket.getOutputStream().flush() - verify(receiver).onConnect(client) + verify(receiver, timeout(1_000)) + .onReceiveMessage(eq(client), any(), eq("SIMAPP"), eq(payload)) } - assertEquals(1, client.connectedDevices.size) - val device: GarminDevice = client.connectedDevices.first() - verify(receiver, timeout(1_000)) - .onReceiveMessage(eq(client), eq(device.id), eq("SIMAPP"), eq(payload)) } @Test From c77d7dedb8b775fcb0a5e9499537b9ea451d83c1 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 8 Dec 2023 09:53:10 +0100 Subject: [PATCH 53/70] fix crash --- .../src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt b/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt index dfcfab28f5..49b808f125 100644 --- a/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt +++ b/core/utils/src/main/kotlin/app/aaps/core/utils/MidnightUtils.kt @@ -19,8 +19,8 @@ object MidnightUtils { fun secondsFromMidnight(): Int { val nowZoned = ZonedDateTime.now() val localTime = nowZoned.toLocalTime() - val midnight: Instant = nowZoned.toLocalDate().atStartOfDay(nowZoned.zone).toInstant() - val duration: Duration = Duration.between(midnight, localTime) + val midnight = nowZoned.toLocalDate().atStartOfDay(nowZoned.zone).toLocalTime() + val duration = Duration.between(midnight, localTime) return duration.seconds.toInt() } @@ -50,7 +50,7 @@ object MidnightUtils { val timeZoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()) val localTime = timeZoned.toLocalTime() val midnight = timeZoned.toLocalDate().atStartOfDay(timeZoned.zone).toLocalTime() - val duration: Duration = Duration.between(midnight, localTime) + val duration = Duration.between(midnight, localTime) return duration.toMillis() } } \ No newline at end of file From 4cdf2e6969029713f22b4d6c781eed431bb94341 Mon Sep 17 00:00:00 2001 From: Robert Buessow Date: Fri, 8 Dec 2023 16:43:42 +0100 Subject: [PATCH 54/70] Fix flaky GarminSimulatorClientTest.sendMessage --- .../sync/garmin/GarminSimulatorClientTest.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt index d2e318ee45..9243260d07 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/garmin/GarminSimulatorClientTest.kt @@ -1,11 +1,9 @@ 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.BeforeEach import org.junit.jupiter.api.Test -import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.isNull import org.mockito.kotlin.mock @@ -20,6 +18,14 @@ class GarminSimulatorClientTest: TestBase() { private lateinit var client: GarminSimulatorClient private val receiver: GarminReceiver = mock() + private fun waitForOrFail(c: ()->T?): T { + for (i in 0 until 10) { + c()?.let { return it } + Thread.sleep(1) + } + throw AssertionError("wait timed out") + } + @BeforeEach fun setup() { client = GarminSimulatorClient(aapsLogger, receiver, 0) @@ -36,8 +42,9 @@ class GarminSimulatorClientTest: TestBase() { assertTrue(socket.isConnected) socket.getOutputStream().write(payload) socket.getOutputStream().flush() + val device = waitForOrFail { client.connectedDevices.firstOrNull() } verify(receiver, timeout(1_000)) - .onReceiveMessage(eq(client), any(), eq("SIMAPP"), eq(payload)) + .onReceiveMessage(eq(client), eq(device.id), eq("SIMAPP"), eq(payload)) } } @@ -45,15 +52,14 @@ class GarminSimulatorClientTest: TestBase() { fun sendMessage() { val payload = "foo".toByteArray() assertTrue(client.awaitReady(Duration.ofSeconds(10))) + verify(receiver, timeout(100)).onConnect(client) val port = client.port val ip = Inet4Address.getByAddress(byteArrayOf(127, 0, 0, 1)) val device: GarminDevice val app: GarminApplication Socket(ip, port).use { socket -> assertTrue(socket.isConnected) - verify(receiver).onConnect(client) - assertEquals(1, client.connectedDevices.size) - device = client.connectedDevices.first() + device = waitForOrFail { client.connectedDevices.firstOrNull() } app = GarminApplication(device, "SIMAPP", "T") client.sendMessage(app, payload) } From 7c9aadc6e0d404c95267f25353790ae4a97425e1 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Sun, 10 Dec 2023 09:52:35 +0100 Subject: [PATCH 55/70] Fix for a misleading string (Garmin -> Samsung) --- plugins/sync/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/sync/src/main/res/values/strings.xml b/plugins/sync/src/main/res/values/strings.xml index 9a85f46131..84c8545aad 100644 --- a/plugins/sync/src/main/res/values/strings.xml +++ b/plugins/sync/src/main/res/values/strings.xml @@ -182,7 +182,7 @@ Data Broadcaster DBRO - Broadcast data to Garmin\'s G-Watch Wear App + Broadcast data to Samsung\'s G-Watch Wear App Garmin @@ -255,4 +255,4 @@ Today weighted - \ No newline at end of file + From f79afa9477404c29455e1448a71d89640db0d816 Mon Sep 17 00:00:00 2001 From: swissalpine Date: Sun, 10 Dec 2023 13:45:16 +0100 Subject: [PATCH 56/70] Add Tizen OS hint --- plugins/sync/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/sync/src/main/res/values/strings.xml b/plugins/sync/src/main/res/values/strings.xml index 84c8545aad..022afb667f 100644 --- a/plugins/sync/src/main/res/values/strings.xml +++ b/plugins/sync/src/main/res/values/strings.xml @@ -182,7 +182,7 @@ Data Broadcaster DBRO - Broadcast data to Samsung\'s G-Watch Wear App + Broadcast data to Samsung\'s G-Watch Wear App (Tizen OS) Garmin From ae1e12c5f6caf014c7a47088a47af4bd44e45bc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 08:55:00 +0000 Subject: [PATCH 57/70] chore(deps): bump org.jlleitschuh.gradle.ktlint from 12.0.2 to 12.0.3 Bumps org.jlleitschuh.gradle.ktlint from 12.0.2 to 12.0.3. --- updated-dependencies: - dependency-name: org.jlleitschuh.gradle.ktlint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index e2bf637c52..19ec3cd8ae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ buildscript { } plugins { - id("org.jlleitschuh.gradle.ktlint") version "12.0.2" + id("org.jlleitschuh.gradle.ktlint") version "12.0.3" } allprojects { From 4f2adcfcae640803d3174e146da9f3b448cd8d26 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 12 Dec 2023 10:36:20 +0100 Subject: [PATCH 58/70] update circleci --- .circleci/config.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b66ac53c71..26f985c0fa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ version: 2.1 # Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects. orbs: android: circleci/android@2.3.0 - codecov: codecov/codecov@3.2.4 + codecov: codecov/codecov@3.3.0 jobs: # Below is the definition of your job to build and test your app, you can rename and customize it as you want. @@ -14,7 +14,7 @@ jobs: executor: name: android/android-machine resource-class: large - tag: 2023.07.1 + tag: 2023.10.1 steps: - checkout @@ -34,11 +34,19 @@ jobs: - android/run-tests: test-command: ./gradlew --stacktrace jacocoAllDebugReport - # And finally run the release build - # - run: - # name: Assemble release build - # command: | - # ./gradlew assembleRelease + - run: + name: Save test results + command: | + mkdir -p ~/test-results/junit/ + find . -type f -regex ".*/build/outputs/androidTest-results/.*xml" -exec cp {} ~/test-results/junit/ \; + when: always + + - store_test_results: + path: ~/test-results + + - store_artifacts: + path: ~/test-results/junit + - codecov/upload: file: './build/reports/jacoco/jacocoAllDebugReport/jacocoAllDebugReport.xml' From 29a88d0f2376595ece48e04e2ffe2b49ee803555 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 12 Dec 2023 10:37:16 +0100 Subject: [PATCH 59/70] New Crowdin updates (#3104) * New translations objectives.xml (Russian) * New translations oh_strings.xml (Russian) * Update source file strings.xml * Update source file strings.xml * Update source file strings.xml * New translations objectives.xml (Polish) --- plugins/constraints/src/main/res/values-pl-rPL/objectives.xml | 2 +- plugins/constraints/src/main/res/values-ru-rRU/objectives.xml | 2 +- plugins/main/src/main/res/values/strings.xml | 2 +- plugins/sync/src/main/res/values-ru-rRU/oh_strings.xml | 4 ++-- plugins/sync/src/main/res/values/strings.xml | 4 ++-- pump/omnipod-common/src/main/res/values/strings.xml | 1 - 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/constraints/src/main/res/values-pl-rPL/objectives.xml b/plugins/constraints/src/main/res/values-pl-rPL/objectives.xml index 09bff75cad..404b9e0b07 100644 --- a/plugins/constraints/src/main/res/values-pl-rPL/objectives.xml +++ b/plugins/constraints/src/main/res/values-pl-rPL/objectives.xml @@ -2,7 +2,7 @@ Start Zweryfikuj - %1$d. Cel + Zadanie %1$d. Zadanie %1$d nierozpoczęte Zadanie %1$d nieukończone Konfiguracja wizualizacji i monitorowania, analiza dawek bazowych i współczynników diff --git a/plugins/constraints/src/main/res/values-ru-rRU/objectives.xml b/plugins/constraints/src/main/res/values-ru-rRU/objectives.xml index bf60945ad5..901b20b971 100644 --- a/plugins/constraints/src/main/res/values-ru-rRU/objectives.xml +++ b/plugins/constraints/src/main/res/values-ru-rRU/objectives.xml @@ -12,7 +12,7 @@ Используйте режим Открытого цикла на протяжении нескольких дней и вручную выставляйте временные цели. Настройте ВЦ по умолчанию (ВЦ для Нагрузки, Гипо, Ожидания приема пищи) и используйте их. Открытый цикл может быть использован для получения рекомендаций по терапии, если у вас нет совместимой помпы или если вы не готовы закрыть цикл. Глубже понимаем работу открытого цикла, включая рекомендации по ВБС - На основе накопленного опыта, определяем максимальную величину базала и задаем ее в помпе и в настройки AndroidAPS + На основе накопленного опыта, определяем максимальную величину базала и задаем ее в помпе и в настройках AAPS Примите меры предосторожности и корректируйте, если необходимо, параметры безопасности. Начинаем замыкать цикл с прекращением подачи инсулина при низких значениях ГК (режим Low Glucose Suspend) Работа в замкнутом цикле с maxIOB = 0 на протяжении нескольких дней. Старайтесь избегать низкой ГК, чтобы не вызывать события приостановки подачи инсулина на низких сахарах. diff --git a/plugins/main/src/main/res/values/strings.xml b/plugins/main/src/main/res/values/strings.xml index ced0a2cf96..a46b55b81e 100644 --- a/plugins/main/src/main/res/values/strings.xml +++ b/plugins/main/src/main/res/values/strings.xml @@ -149,7 +149,7 @@ add new to list Do you want to switch profile and discard changes made to current profile? Save or reset current changes first - Delete profile \"%1$s\"? + Delete current profile? Units: Missing profile name Error in IC values diff --git a/plugins/sync/src/main/res/values-ru-rRU/oh_strings.xml b/plugins/sync/src/main/res/values-ru-rRU/oh_strings.xml index 03f89ca280..b497eb4703 100644 --- a/plugins/sync/src/main/res/values-ru-rRU/oh_strings.xml +++ b/plugins/sync/src/main/res/values-ru-rRU/oh_strings.xml @@ -22,7 +22,7 @@ Условия пользования Внимательно ознакомьтесь со следующей информацией и примите условия использования. Это проект с открытым исходным кодом, который будет копировать ваши данные на Open Humans. Мы не будем обмениваться вашими данными с третьими лицами без вашего явного разрешения. Данные, отправляемые на проект и приложение, идентифицируются с помощью случайного идентификатора и будут безопасно передаваться на учетную запись Open Humans при вашем одобрении этого процесса. Вы можете остановить загрузку и удалить загруженные данные в любое время здесь: www.openhumans.org. - Данные загружены + Загружаемые данные Гликемия Болюсы Пролонгированные болюсы @@ -37,7 +37,7 @@ Модель устройства Размеры экрана Данные отладки алгоритма - Данные НЕ загружены + НЕзагружаемые данные Пароли URL-адрес Nightscout Секретный ключ Nightscout API diff --git a/plugins/sync/src/main/res/values/strings.xml b/plugins/sync/src/main/res/values/strings.xml index 022afb667f..9a85f46131 100644 --- a/plugins/sync/src/main/res/values/strings.xml +++ b/plugins/sync/src/main/res/values/strings.xml @@ -182,7 +182,7 @@ Data Broadcaster DBRO - Broadcast data to Samsung\'s G-Watch Wear App (Tizen OS) + Broadcast data to Garmin\'s G-Watch Wear App Garmin @@ -255,4 +255,4 @@ Today weighted - + \ No newline at end of file diff --git a/pump/omnipod-common/src/main/res/values/strings.xml b/pump/omnipod-common/src/main/res/values/strings.xml index 0a2757f7c5..d2ecea3ce0 100644 --- a/pump/omnipod-common/src/main/res/values/strings.xml +++ b/pump/omnipod-common/src/main/res/values/strings.xml @@ -158,7 +158,6 @@ Activation time exceeded Inactive Pod Fault: %1$03d %2$s - Normal Deactivate Pod From f13c70bbdec1ef2146a97aa62671aa4245625728 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 12 Dec 2023 11:22:33 +0100 Subject: [PATCH 60/70] update circleci image --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 26f985c0fa..4593de71fc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ jobs: executor: name: android/android-machine resource-class: large - tag: 2023.10.1 + tag: 2023.11.1 steps: - checkout From 8ce178bce2f2115195e11d405ae115786aa07d83 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Tue, 12 Dec 2023 11:47:11 +0100 Subject: [PATCH 61/70] add missing string --- pump/omnipod-common/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pump/omnipod-common/src/main/res/values/strings.xml b/pump/omnipod-common/src/main/res/values/strings.xml index d2ecea3ce0..b866480e23 100644 --- a/pump/omnipod-common/src/main/res/values/strings.xml +++ b/pump/omnipod-common/src/main/res/values/strings.xml @@ -154,6 +154,7 @@ Setup in progress (waiting for cannula insertion) Running Suspended + Normal Pod Fault Activation time exceeded Inactive From 46be02573a3bbb909111c4c99d6fa9c388d72727 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 13 Dec 2023 10:44:24 +0100 Subject: [PATCH 62/70] New Crowdin updates (#3107) * Update source file strings.xml * New translations strings.xml (Dutch) --- plugins/aps/src/main/res/values-nl-rNL/strings.xml | 2 ++ pump/omnipod-common/src/main/res/values/strings.xml | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/aps/src/main/res/values-nl-rNL/strings.xml b/plugins/aps/src/main/res/values-nl-rNL/strings.xml index 1b23289673..7530c62297 100644 --- a/plugins/aps/src/main/res/values-nl-rNL/strings.xml +++ b/plugins/aps/src/main/res/values-nl-rNL/strings.xml @@ -1,5 +1,7 @@ + Inschakelen van TDD gebaseerde gevoeligheid ratio voor basaal en glucose doel aanpassingen + Gebruikt de laatste 24u TDD/7D om de gevoeligheid te berekenen voor het verhogen of verlagen van de basaalstand en past het glucosedoel aan als deze opties zijn ingeschakeld, op dezelfde manier als Autosens dat doet. Advies is om te starten met deze optie uitgeschakeld Dynamische Isf aanpassingsfactor % Aanpassingsfactor voor DynamicISF. Stel meer dan 100% in voor een agressievere correctie en minder dan 100% voor minder agressieve correctie. Hoog tijdelijk streefdoel verhoogt gevoeligheid diff --git a/pump/omnipod-common/src/main/res/values/strings.xml b/pump/omnipod-common/src/main/res/values/strings.xml index b866480e23..d2ecea3ce0 100644 --- a/pump/omnipod-common/src/main/res/values/strings.xml +++ b/pump/omnipod-common/src/main/res/values/strings.xml @@ -154,7 +154,6 @@ Setup in progress (waiting for cannula insertion) Running Suspended - Normal Pod Fault Activation time exceeded Inactive From adf3b6fe4b0f187d3df0afb9cbfe190631fe2160 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 13 Dec 2023 12:46:04 +0100 Subject: [PATCH 63/70] 3.2.0.3 --- buildSrc/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index dec215bada..17b2346624 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -2,7 +2,7 @@ import org.gradle.api.JavaVersion object Versions { - const val appVersion = "3.2.0.2-dev" + const val appVersion = "3.2.0.3" const val versionCode = 1500 const val ndkVersion = "21.1.6352462" From 8498fcb358b489ef41ac92babc6d9c1867439042 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 13 Dec 2023 13:08:24 +0100 Subject: [PATCH 64/70] DataBroadcaster rename --- plugins/sync/src/main/res/values/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/sync/src/main/res/values/strings.xml b/plugins/sync/src/main/res/values/strings.xml index 9a85f46131..2837021f3d 100644 --- a/plugins/sync/src/main/res/values/strings.xml +++ b/plugins/sync/src/main/res/values/strings.xml @@ -180,9 +180,9 @@ - Data Broadcaster - DBRO - Broadcast data to Garmin\'s G-Watch Wear App + Samsung Tizen + TIZ + Broadcast data to Samsung\'s G-Watch Wear App (Tizen OS) Garmin From 39eb458e6cdbd4664cf88d116d25332c61274cf4 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Wed, 13 Dec 2023 13:30:51 +0100 Subject: [PATCH 65/70] add missing string --- pump/omnipod-common/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pump/omnipod-common/src/main/res/values/strings.xml b/pump/omnipod-common/src/main/res/values/strings.xml index d2ecea3ce0..72eb74947a 100644 --- a/pump/omnipod-common/src/main/res/values/strings.xml +++ b/pump/omnipod-common/src/main/res/values/strings.xml @@ -153,6 +153,7 @@ Setup in progress (waiting for Pod activation) Setup in progress (waiting for cannula insertion) Running + Normal Suspended Pod Fault Activation time exceeded From d7bc09c7e0cc8b85d0a4781d4867bbd2ce69b04f Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 14 Dec 2023 11:15:56 +0100 Subject: [PATCH 66/70] DataBroadcaster -> Tizen --- app/src/main/kotlin/app/aaps/di/PluginsListModule.kt | 4 ++-- .../DataBroadcastPlugin.kt => tizen/TizenPlugin.kt} | 10 +++++----- plugins/sync/src/main/res/values/strings.xml | 6 +++--- .../TizenPluginTest.kt} | 8 ++++---- 4 files changed, 14 insertions(+), 14 deletions(-) rename plugins/sync/src/main/kotlin/app/aaps/plugins/sync/{dataBroadcaster/DataBroadcastPlugin.kt => tizen/TizenPlugin.kt} (97%) rename plugins/sync/src/test/kotlin/app/aaps/plugins/sync/{dataBroadcaster/DataBroadcastPluginTest.kt => tizen/TizenPluginTest.kt} (97%) diff --git a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt index 6ea627c117..36c1cd22c0 100644 --- a/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt +++ b/app/src/main/kotlin/app/aaps/di/PluginsListModule.kt @@ -47,7 +47,7 @@ import app.aaps.plugins.source.PoctechPlugin import app.aaps.plugins.source.RandomBgPlugin import app.aaps.plugins.source.TomatoPlugin import app.aaps.plugins.source.XdripSourcePlugin -import app.aaps.plugins.sync.dataBroadcaster.DataBroadcastPlugin +import app.aaps.plugins.sync.tizen.TizenPlugin import app.aaps.plugins.sync.nsclient.NSClientPlugin import app.aaps.plugins.sync.nsclientV3.NSClientV3Plugin import app.aaps.plugins.sync.openhumans.OpenHumansUploaderPlugin @@ -350,7 +350,7 @@ abstract class PluginsListModule { @AllConfigs @IntoMap @IntKey(368) - abstract fun bindDataBroadcastPlugin(plugin: DataBroadcastPlugin): PluginBase + abstract fun bindDataBroadcastPlugin(plugin: TizenPlugin): PluginBase @Binds @AllConfigs diff --git a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt similarity index 97% rename from plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt rename to plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt index 6ee7128b16..3f90c172c9 100644 --- a/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPlugin.kt +++ b/plugins/sync/src/main/kotlin/app/aaps/plugins/sync/tizen/TizenPlugin.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.sync.dataBroadcaster +package app.aaps.plugins.sync.tizen import android.content.Context import android.content.Intent @@ -41,7 +41,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class DataBroadcastPlugin @Inject constructor( +class TizenPlugin @Inject constructor( injector: HasAndroidInjector, aapsLogger: AAPSLogger, rh: ResourceHelper, @@ -64,9 +64,9 @@ class DataBroadcastPlugin @Inject constructor( PluginDescription() .mainType(PluginType.SYNC) .pluginIcon(app.aaps.core.main.R.drawable.ic_watch) - .pluginName(R.string.data_broadcaster) - .shortName(R.string.data_broadcaster_short) - .description(R.string.data_broadcaster_description), + .pluginName(R.string.tizen) + .shortName(R.string.tizen_short) + .description(R.string.tizen_description), aapsLogger, rh, injector ) { diff --git a/plugins/sync/src/main/res/values/strings.xml b/plugins/sync/src/main/res/values/strings.xml index 2837021f3d..266db55530 100644 --- a/plugins/sync/src/main/res/values/strings.xml +++ b/plugins/sync/src/main/res/values/strings.xml @@ -180,9 +180,9 @@ - Samsung Tizen - TIZ - Broadcast data to Samsung\'s G-Watch Wear App (Tizen OS) + Samsung Tizen + TIZ + Broadcast data to Samsung\'s G-Watch Wear App (Tizen OS) Garmin diff --git a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPluginTest.kt b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt similarity index 97% rename from plugins/sync/src/test/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPluginTest.kt rename to plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt index 650eaa7bb5..fc42eba307 100644 --- a/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/dataBroadcaster/DataBroadcastPluginTest.kt +++ b/plugins/sync/src/test/kotlin/app/aaps/plugins/sync/tizen/TizenPluginTest.kt @@ -1,4 +1,4 @@ -package app.aaps.plugins.sync.dataBroadcaster +package app.aaps.plugins.sync.tizen import app.aaps.core.interfaces.aps.AutosensDataStore import app.aaps.core.interfaces.aps.Loop @@ -27,7 +27,7 @@ import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mock import org.mockito.Mockito -internal class DataBroadcastPluginTest : TestBaseWithProfile() { +internal class TizenPluginTest : TestBaseWithProfile() { @Mock lateinit var defaultValueHelper: DefaultValueHelper @Mock lateinit var loop: Loop @@ -36,13 +36,13 @@ internal class DataBroadcastPluginTest : TestBaseWithProfile() { @Mock lateinit var autosensDataStore: AutosensDataStore @Mock lateinit var processedDeviceStatusData: ProcessedDeviceStatusData - private lateinit var sut: DataBroadcastPlugin + private lateinit var sut: TizenPlugin private val injector = HasAndroidInjector { AndroidInjector { } } @BeforeEach fun setUp() { - sut = DataBroadcastPlugin( + sut = TizenPlugin( injector, aapsLogger, rh, aapsSchedulers, context, dateUtil, fabricPrivacy, rxBus, iobCobCalculator, profileFunction, defaultValueHelper, processedDeviceStatusData, loop, activePlugin, receiverStatusStore, config, glucoseStatusProvider, decimalFormatter ) From 77d6f683b336d20301378fc2bfee6af2f285d3ef Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Thu, 14 Dec 2023 14:57:50 +0100 Subject: [PATCH 67/70] New Crowdin updates (#3110) * Update source file strings.xml * New translations strings.xml (Spanish) * New translations strings.xml (Czech) * New translations strings.xml (Hebrew) * New translations strings.xml (Polish) * New translations strings.xml (Slovak) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Dutch) * New translations strings.xml (Norwegian Bokmal) * New translations strings.xml (Romanian) * New translations strings.xml (French) * New translations strings.xml (Spanish) * New translations strings.xml (Czech) * New translations strings.xml (Italian) * New translations strings.xml (Lithuanian) * New translations strings.xml (Dutch) * New translations strings.xml (Polish) * New translations strings.xml (Russian) * New translations strings.xml (Slovak) * New translations strings.xml (Turkish) * New translations strings.xml (Norwegian Bokmal) * Update source file strings.xml * New translations strings.xml (Italian) --- plugins/sync/src/main/res/values-cs-rCZ/strings.xml | 5 +++-- plugins/sync/src/main/res/values-es-rES/strings.xml | 2 -- plugins/sync/src/main/res/values-fr-rFR/strings.xml | 3 --- plugins/sync/src/main/res/values-it-rIT/strings.xml | 5 +++-- plugins/sync/src/main/res/values-lt-rLT/strings.xml | 1 - plugins/sync/src/main/res/values-nb-rNO/strings.xml | 2 -- plugins/sync/src/main/res/values-nl-rNL/strings.xml | 2 -- plugins/sync/src/main/res/values-pl-rPL/strings.xml | 2 -- plugins/sync/src/main/res/values-ro-rRO/strings.xml | 2 -- plugins/sync/src/main/res/values-ru-rRU/strings.xml | 2 -- plugins/sync/src/main/res/values-sk-rSK/strings.xml | 2 -- plugins/sync/src/main/res/values-tr-rTR/strings.xml | 2 -- pump/omnipod-common/src/main/res/values-cs-rCZ/strings.xml | 1 + pump/omnipod-common/src/main/res/values-es-rES/strings.xml | 1 + pump/omnipod-common/src/main/res/values-iw-rIL/strings.xml | 1 + pump/omnipod-common/src/main/res/values-nb-rNO/strings.xml | 1 + pump/omnipod-common/src/main/res/values-nl-rNL/strings.xml | 1 + pump/omnipod-common/src/main/res/values-pl-rPL/strings.xml | 1 + pump/omnipod-common/src/main/res/values-sk-rSK/strings.xml | 1 + 19 files changed, 13 insertions(+), 24 deletions(-) diff --git a/plugins/sync/src/main/res/values-cs-rCZ/strings.xml b/plugins/sync/src/main/res/values-cs-rCZ/strings.xml index 870a05b4af..e557bea998 100644 --- a/plugins/sync/src/main/res/values-cs-rCZ/strings.xml +++ b/plugins/sync/src/main/res/values-cs-rCZ/strings.xml @@ -117,8 +117,9 @@ Poslat data o glykémii a ošetření do xDrip+. Musí být vybrán zdroj dat \"xDrip+ Sync Follower\" a přijímání dat musí být povoleno v Nastavení - Nastavení komunikace mezi aplikacemi - Přijímat Glykémie/Ošetření Povolit odesílání do xDrip+. - DBRO - Odesílání dat do Garmin aplikace G-Watch Wear App + Samsung Tizen + TIZ + Odesílání dat do Samsung aplikace G-Watch Wear App (Tizen OS) Garmin Připojení k zařízení Garmin (Fenix, Edge, …) diff --git a/plugins/sync/src/main/res/values-es-rES/strings.xml b/plugins/sync/src/main/res/values-es-rES/strings.xml index 0c777ef050..4cc1e18c35 100644 --- a/plugins/sync/src/main/res/values-es-rES/strings.xml +++ b/plugins/sync/src/main/res/values-es-rES/strings.xml @@ -117,8 +117,6 @@ Enviar datos de glucosa y tratamientos a xDrip+. La fuente de datos \"xDrip+ Sync Follower\" debe estar seleccionada y la aceptación de datos debe estar activada en Ajustes - Ajustes entre aplicaciones - Aceptar glucosa/tratamientos Activar las transmisiones a xDrip+ - DBRO - Transmitir datos a la aplicación G-Watch Wear de Garmin Garmin Conexión al dispositivo Garmin (Fénix, Edge, …) diff --git a/plugins/sync/src/main/res/values-fr-rFR/strings.xml b/plugins/sync/src/main/res/values-fr-rFR/strings.xml index c5fbc87a84..9607d003ad 100644 --- a/plugins/sync/src/main/res/values-fr-rFR/strings.xml +++ b/plugins/sync/src/main/res/values-fr-rFR/strings.xml @@ -117,9 +117,6 @@ Envoyer les glycémies et les traitements à xDrip+. La source de données \"xDrip+ Sync Follower\" doit être sélectionnée et l\'acceptation des données doit être activée dans Paramètres - Paramètres Inter-app - Accepter Glycémies/Traitements Activer les diffusions vers xDrip+. - Diffuseur de données - DD - Diffuser des données sur l\'application G-Watch Wear de Garmin Garmin Connexion au périphérique Garmin (Fenix, Edge, …) diff --git a/plugins/sync/src/main/res/values-it-rIT/strings.xml b/plugins/sync/src/main/res/values-it-rIT/strings.xml index 1e821f5299..91bec16496 100644 --- a/plugins/sync/src/main/res/values-it-rIT/strings.xml +++ b/plugins/sync/src/main/res/values-it-rIT/strings.xml @@ -117,8 +117,9 @@ Invia dati glicemia e trattamenti a xDrip+. La sorgente dati \"xDrip+ Sync Follower\" deve essere selezionata e l\'accettazione dei dati deve essere abilitata in: Settings - Inter-app settings - Accept Glucose/Treatments Abilita trasmissioni a xDrip+. - DBRO - Trasmetti i dati all\'app G-Watch Wear di Garmin + Samsung Tizen + TIZ + Trasmetti i dati all\'app G-Watch Wear di Samsung (Tizen OS) Garmin Connessione al dispositivo Garmin (Fenix, Edge, …) diff --git a/plugins/sync/src/main/res/values-lt-rLT/strings.xml b/plugins/sync/src/main/res/values-lt-rLT/strings.xml index fd27843caf..c06aae11d4 100644 --- a/plugins/sync/src/main/res/values-lt-rLT/strings.xml +++ b/plugins/sync/src/main/res/values-lt-rLT/strings.xml @@ -117,7 +117,6 @@ Siųsti glikemijos ir terapijos duomenis į xDrip+. Turi būti pasirinktas duomenų šaltinis „xDrip+ Sync Follower“, o duomenų priėmimas turi būti įjungtas skiltyje Nustatymai – Programų sąveikos nustatymai – Priimti glikemiją/terapiją Įjungti duomenų perdavimą į xDrip+. - Data Broadcaster Garmin Susiejimas su Garmin (Fenix, Edge, …) diff --git a/plugins/sync/src/main/res/values-nb-rNO/strings.xml b/plugins/sync/src/main/res/values-nb-rNO/strings.xml index 0e99568379..da87ecddfd 100644 --- a/plugins/sync/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/sync/src/main/res/values-nb-rNO/strings.xml @@ -117,8 +117,6 @@ Send data om glukose og behandling til xDrip+. Velg datakilde \"xDrip+ Sync Følger\" og aktiver mottak av data under Innstillinger - Inter-app innstillinger - Aksepter glukose/behandlinger Aktiver sending til xDrip+. - DBRO - Kringkast data til Garmin\'s G-Watch Wear app Garmin Tilkobling til Garmin-enheter (Fenix, Edge,…) diff --git a/plugins/sync/src/main/res/values-nl-rNL/strings.xml b/plugins/sync/src/main/res/values-nl-rNL/strings.xml index 477feec8a6..fff5b22634 100644 --- a/plugins/sync/src/main/res/values-nl-rNL/strings.xml +++ b/plugins/sync/src/main/res/values-nl-rNL/strings.xml @@ -117,8 +117,6 @@ Verzend glucose en behandelingsgegevens naar xDrip+. Gegevensbron \"xDrip+ Sync Follower\" moet worden geselecteerd en het accepteren van gegevens moet worden ingeschakeld in Instellingen - Inter-app instellingen - Accepteer Glucose/Behandelingen Activeer uitzendingen naar xDrip+. - DBRO - Verzend gegevens naar Garmin\'s G-Watch Wear App Garmin Verbinding met Garmin apparaat (Fenix, Edge, …) diff --git a/plugins/sync/src/main/res/values-pl-rPL/strings.xml b/plugins/sync/src/main/res/values-pl-rPL/strings.xml index d444294137..22043712ab 100644 --- a/plugins/sync/src/main/res/values-pl-rPL/strings.xml +++ b/plugins/sync/src/main/res/values-pl-rPL/strings.xml @@ -117,8 +117,6 @@ Wyślij dane dotyczące glikemii i leczenia do xDrip+. W ustawieniach xDrip+ należy ustawić \"Sprzętowe źródło danych\" na \"xDrip+ Sync Follower\" oraz włączyć akceptowanie danych: \"Ustawienia innych aplikacji\" - \"Akceptuj Glukozę/Akceptuj zabiegi\" Włącz nadawanie do xDrip+. - DBRO - Transmisja danych do aplikacji G-Watch Garmin Garmin Połączenie z urządzeniem Garmin (Fenix, Edge, …) diff --git a/plugins/sync/src/main/res/values-ro-rRO/strings.xml b/plugins/sync/src/main/res/values-ro-rRO/strings.xml index 712f43398e..49e0de8e0b 100644 --- a/plugins/sync/src/main/res/values-ro-rRO/strings.xml +++ b/plugins/sync/src/main/res/values-ro-rRO/strings.xml @@ -117,8 +117,6 @@ Trimite date despre glucoză și tratamente către xDrip+. Trebuie să fie selectată sursa de date \"Sincronizare xDrip+ Urmăritor\" și acceptarea datelor trebuie să fie activată în „Setări - Setări între aplicații - Acceptă Glucoză/Tratamente” Activează transmisiuni spre xDrip+. - DBRO - Transmitere date către aplicaţia G-Watch Wear pe Garmin Garmin Conexiune la dispozitivul Garmin (Fenix, Edge, …) diff --git a/plugins/sync/src/main/res/values-ru-rRU/strings.xml b/plugins/sync/src/main/res/values-ru-rRU/strings.xml index a2498c8e08..8d2c317514 100644 --- a/plugins/sync/src/main/res/values-ru-rRU/strings.xml +++ b/plugins/sync/src/main/res/values-ru-rRU/strings.xml @@ -117,8 +117,6 @@ Отправлять данные о глюкозе и терапии на xDrip+. Источником данных должен быть выбран \"xDrip+ Sync Follower\" а в настройках между приложениями надо включить - Принимать глюкозу/терапию Включить трансляции для xDrip+. - DBRO - Передавать данные приложению Garmin\'s G-Watch Wear Garmin Подключение к устройству Garmin (Fenix, Edge, …) diff --git a/plugins/sync/src/main/res/values-sk-rSK/strings.xml b/plugins/sync/src/main/res/values-sk-rSK/strings.xml index 83b5b878f1..1ed7b67f51 100644 --- a/plugins/sync/src/main/res/values-sk-rSK/strings.xml +++ b/plugins/sync/src/main/res/values-sk-rSK/strings.xml @@ -117,8 +117,6 @@ Poslať dáta o glykémii a ošetrení do xDrip+. Musí byť vybraný zdroj dát \"xDrip+ Sync Follower\" a prijímanie dát musí byť povolené v Nastavenia - Nastavenie komunikácie medzi aplikáciami - Prijímať Glykémie/Ošetrenia Povoliť odosielanie do xDrip+. - DBRO - Odosielanie dát do Garmin aplikácie G-Watch Wear Garmin Pripájanie k zariadeniu Garmin (Fénix, Edge, ...) diff --git a/plugins/sync/src/main/res/values-tr-rTR/strings.xml b/plugins/sync/src/main/res/values-tr-rTR/strings.xml index c90441313e..6fd16ff6af 100644 --- a/plugins/sync/src/main/res/values-tr-rTR/strings.xml +++ b/plugins/sync/src/main/res/values-tr-rTR/strings.xml @@ -117,8 +117,6 @@ KŞ ve tedavi verilerini xDrip+\'a gönderin. Veri Kaynağı \"xDrip+ Sync Follower\" seçilmeli ve Ayarlar - Uygulamalar arası ayarlar - KŞ/Tedavileri Kabul Et bölümünde verilerin kabul edilmesi etkinleştirilmelidir. xDrip+ \'a yayınları etkinleştirin. - DBRO - Verileri Garmin\'in G-Watch Wear Uygulamasına yayınlayın Garmin Garmin cihazına bağlantı (Fenix, Edge,…) diff --git a/pump/omnipod-common/src/main/res/values-cs-rCZ/strings.xml b/pump/omnipod-common/src/main/res/values-cs-rCZ/strings.xml index 12261e0feb..056fffdff3 100644 --- a/pump/omnipod-common/src/main/res/values-cs-rCZ/strings.xml +++ b/pump/omnipod-common/src/main/res/values-cs-rCZ/strings.xml @@ -124,6 +124,7 @@ Probíhá nastavování (čeká se na aktivaci Podu) Probíhá nastavování (čeká se na aplikaci kanyly) Běží + Normální Pozastaveno Chyba Podu Byl překročen čas aktivace diff --git a/pump/omnipod-common/src/main/res/values-es-rES/strings.xml b/pump/omnipod-common/src/main/res/values-es-rES/strings.xml index 06aa635503..9ac89e5d5b 100644 --- a/pump/omnipod-common/src/main/res/values-es-rES/strings.xml +++ b/pump/omnipod-common/src/main/res/values-es-rES/strings.xml @@ -125,6 +125,7 @@ Configuración en curso (esperando para activar el Pod) Configuración en curso (en espera de inserción de cánula) Funcionando + Normal Suspendido Error del Pod Tiempo de activación excedido diff --git a/pump/omnipod-common/src/main/res/values-iw-rIL/strings.xml b/pump/omnipod-common/src/main/res/values-iw-rIL/strings.xml index 6f0687373e..7f2172f527 100644 --- a/pump/omnipod-common/src/main/res/values-iw-rIL/strings.xml +++ b/pump/omnipod-common/src/main/res/values-iw-rIL/strings.xml @@ -124,6 +124,7 @@ התקנה בביצוע (ממתין להפעלת הפוד) התקנה בביצוע (ממתין להכנסת הפרפרית) פועל + נורמלי מושהה תקלה בפוד חריגת זמן הפעלה diff --git a/pump/omnipod-common/src/main/res/values-nb-rNO/strings.xml b/pump/omnipod-common/src/main/res/values-nb-rNO/strings.xml index 121dfd1331..72339fdab6 100644 --- a/pump/omnipod-common/src/main/res/values-nb-rNO/strings.xml +++ b/pump/omnipod-common/src/main/res/values-nb-rNO/strings.xml @@ -124,6 +124,7 @@ Oppsett pågår (venter på Pod-aktivering) Oppsett pågår (venter på innsetting av kanyle) Kjører + Normal Pauset Pod-feil Aktiveringstiden er overskredet diff --git a/pump/omnipod-common/src/main/res/values-nl-rNL/strings.xml b/pump/omnipod-common/src/main/res/values-nl-rNL/strings.xml index 8502534c17..580524dd47 100644 --- a/pump/omnipod-common/src/main/res/values-nl-rNL/strings.xml +++ b/pump/omnipod-common/src/main/res/values-nl-rNL/strings.xml @@ -124,6 +124,7 @@ Setup wordt uitgevoerd (in afwachting van Pod activering) Setup wordt uitgevoerd (in afwachting van het inbrengen van de canule) Actief + Normaal Onderbroken Pod fout Activatie tijd verlopen diff --git a/pump/omnipod-common/src/main/res/values-pl-rPL/strings.xml b/pump/omnipod-common/src/main/res/values-pl-rPL/strings.xml index 80523e2bdf..f8ac9ee127 100644 --- a/pump/omnipod-common/src/main/res/values-pl-rPL/strings.xml +++ b/pump/omnipod-common/src/main/res/values-pl-rPL/strings.xml @@ -124,6 +124,7 @@ Konfiguracja w toku (oczekiwanie na aktywację Poda) Konfiguracja w toku (oczekiwanie na wprowadzenie kaniuli) W działaniu + Normalny Wstrzymany Błąd Poda Przekroczono czas aktywacji diff --git a/pump/omnipod-common/src/main/res/values-sk-rSK/strings.xml b/pump/omnipod-common/src/main/res/values-sk-rSK/strings.xml index e5a0004542..cb26c7ee90 100644 --- a/pump/omnipod-common/src/main/res/values-sk-rSK/strings.xml +++ b/pump/omnipod-common/src/main/res/values-sk-rSK/strings.xml @@ -124,6 +124,7 @@ Prebieha inštalácia (čaká sa na aktiváciu Podu) Inštalácia prebieha (čaká sa na vloženie kanyly) V prevádzke + Normálny Pozastavené Chyba Podu Bol prekročený čas aktivácie From 01321176f8014d8322ac5b4a3e020dcc6cdbd721 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 15 Dec 2023 07:58:06 +0100 Subject: [PATCH 68/70] New Crowdin updates (#3113) * New translations strings.xml (Russian) * New translations strings.xml (Russian) * New translations strings.xml (Norwegian Bokmal) --- plugins/sync/src/main/res/values-nb-rNO/strings.xml | 3 +++ plugins/sync/src/main/res/values-ru-rRU/strings.xml | 3 +++ pump/omnipod-common/src/main/res/values-ru-rRU/strings.xml | 1 + 3 files changed, 7 insertions(+) diff --git a/plugins/sync/src/main/res/values-nb-rNO/strings.xml b/plugins/sync/src/main/res/values-nb-rNO/strings.xml index da87ecddfd..7af2272a92 100644 --- a/plugins/sync/src/main/res/values-nb-rNO/strings.xml +++ b/plugins/sync/src/main/res/values-nb-rNO/strings.xml @@ -117,6 +117,9 @@ Send data om glukose og behandling til xDrip+. Velg datakilde \"xDrip+ Sync Følger\" og aktiver mottak av data under Innstillinger - Inter-app innstillinger - Aksepter glukose/behandlinger Aktiver sending til xDrip+. + Samsung Tizen + TIZ + Kringkast data til Samsung\'s G-Watch Wear app (Tizen OS) Garmin Tilkobling til Garmin-enheter (Fenix, Edge,…) diff --git a/plugins/sync/src/main/res/values-ru-rRU/strings.xml b/plugins/sync/src/main/res/values-ru-rRU/strings.xml index 8d2c317514..b3daffd9c0 100644 --- a/plugins/sync/src/main/res/values-ru-rRU/strings.xml +++ b/plugins/sync/src/main/res/values-ru-rRU/strings.xml @@ -117,6 +117,9 @@ Отправлять данные о глюкозе и терапии на xDrip+. Источником данных должен быть выбран \"xDrip+ Sync Follower\" а в настройках между приложениями надо включить - Принимать глюкозу/терапию Включить трансляции для xDrip+. + Samsung Tizen + TIZ + Передавать данные приложению Samsung G-Watch Wear (OS Tizen) Garmin Подключение к устройству Garmin (Fenix, Edge, …) diff --git a/pump/omnipod-common/src/main/res/values-ru-rRU/strings.xml b/pump/omnipod-common/src/main/res/values-ru-rRU/strings.xml index efa7578782..a04fce5329 100644 --- a/pump/omnipod-common/src/main/res/values-ru-rRU/strings.xml +++ b/pump/omnipod-common/src/main/res/values-ru-rRU/strings.xml @@ -124,6 +124,7 @@ Выполняется настройка (ожидание активации Pod) Выполняется настройка (ожидание ввода катетера) Выполняется + Норма Приостановлено Ошибка Pod Превышено время активации From af451795796a72138095003264daf99142d0ea86 Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Fri, 15 Dec 2023 09:58:05 +0100 Subject: [PATCH 69/70] New Crowdin updates (#3114) * New translations strings.xml (Turkish) * New translations strings.xml (Turkish) --- plugins/sync/src/main/res/values-tr-rTR/strings.xml | 3 +++ pump/omnipod-common/src/main/res/values-tr-rTR/strings.xml | 1 + 2 files changed, 4 insertions(+) diff --git a/plugins/sync/src/main/res/values-tr-rTR/strings.xml b/plugins/sync/src/main/res/values-tr-rTR/strings.xml index 6fd16ff6af..8f02ae2630 100644 --- a/plugins/sync/src/main/res/values-tr-rTR/strings.xml +++ b/plugins/sync/src/main/res/values-tr-rTR/strings.xml @@ -117,6 +117,9 @@ KŞ ve tedavi verilerini xDrip+\'a gönderin. Veri Kaynağı \"xDrip+ Sync Follower\" seçilmeli ve Ayarlar - Uygulamalar arası ayarlar - KŞ/Tedavileri Kabul Et bölümünde verilerin kabul edilmesi etkinleştirilmelidir. xDrip+ \'a yayınları etkinleştirin. + Samsung Tizen + TIZ + Verileri Samsung\'un G-Watch Wear Uygulamasına (Tizen OS) yayınlayın Garmin Garmin cihazına bağlantı (Fenix, Edge,…) diff --git a/pump/omnipod-common/src/main/res/values-tr-rTR/strings.xml b/pump/omnipod-common/src/main/res/values-tr-rTR/strings.xml index 637090f012..dd668038aa 100644 --- a/pump/omnipod-common/src/main/res/values-tr-rTR/strings.xml +++ b/pump/omnipod-common/src/main/res/values-tr-rTR/strings.xml @@ -124,6 +124,7 @@ Kurulum devam ediyor (Pod aktivasyonu bekleniyor) Kurulum devam ediyor (kanül yerleştirme bekleniyor) Çalışıyor + Normal Askıya Alındı Pod Hatası Aktivasyon süresi aşıldı From 8d50cb7e21c96c78e0e3b1c5480d36f11393955e Mon Sep 17 00:00:00 2001 From: Milos Kozak Date: Sat, 16 Dec 2023 20:31:40 +0100 Subject: [PATCH 70/70] New Crowdin updates (#3115) * New translations strings.xml (Slovak) * New translations strings.xml (Romanian) * New translations strings.xml (Romanian) * New translations strings.xml (Romanian) --- plugins/aps/src/main/res/values-ro-rRO/strings.xml | 2 ++ plugins/sync/src/main/res/values-ro-rRO/strings.xml | 3 +++ plugins/sync/src/main/res/values-sk-rSK/strings.xml | 3 +++ pump/omnipod-common/src/main/res/values-ro-rRO/strings.xml | 1 + 4 files changed, 9 insertions(+) diff --git a/plugins/aps/src/main/res/values-ro-rRO/strings.xml b/plugins/aps/src/main/res/values-ro-rRO/strings.xml index 51754ad6c4..c3f58255cc 100644 --- a/plugins/aps/src/main/res/values-ro-rRO/strings.xml +++ b/plugins/aps/src/main/res/values-ro-rRO/strings.xml @@ -1,5 +1,7 @@ + Activează raportul de sensibilitate bazat pe TDD pentru modificarea bazalei și a țintei glicemice + Folosește ultimele 24 de ore TDD/7D TDD pentru a calcula raportul de sensibilitate utilizat pentru creșterea sau scăderea ratei bazale, şi de asemenea ajustează ţinta glicemic[ dacă aceste opţiuni sunt activate, în acelaşi mod în care procedează Autosens. Este recomandat ca opțiunea să fie oprită la început Factor de ajustare dinamic % Factorul de ajustare pentru DynamicISF. Setează mai mult de 100% pentru doze de corecție mai agresive și mai puțin de 100% pentru corecții mai puțin agresive. Ținte temporare mai mari cresc sensibilitatea diff --git a/plugins/sync/src/main/res/values-ro-rRO/strings.xml b/plugins/sync/src/main/res/values-ro-rRO/strings.xml index 49e0de8e0b..841bfeff1e 100644 --- a/plugins/sync/src/main/res/values-ro-rRO/strings.xml +++ b/plugins/sync/src/main/res/values-ro-rRO/strings.xml @@ -117,6 +117,9 @@ Trimite date despre glucoză și tratamente către xDrip+. Trebuie să fie selectată sursa de date \"Sincronizare xDrip+ Urmăritor\" și acceptarea datelor trebuie să fie activată în „Setări - Setări între aplicații - Acceptă Glucoză/Tratamente” Activează transmisiuni spre xDrip+. + Samsung Tizen + TIZ + Transmitere de date către aplicaţia Samsung G-Watch Wear (TizenOS) Garmin Conexiune la dispozitivul Garmin (Fenix, Edge, …) diff --git a/plugins/sync/src/main/res/values-sk-rSK/strings.xml b/plugins/sync/src/main/res/values-sk-rSK/strings.xml index 1ed7b67f51..798a6d17f1 100644 --- a/plugins/sync/src/main/res/values-sk-rSK/strings.xml +++ b/plugins/sync/src/main/res/values-sk-rSK/strings.xml @@ -117,6 +117,9 @@ Poslať dáta o glykémii a ošetrení do xDrip+. Musí byť vybraný zdroj dát \"xDrip+ Sync Follower\" a prijímanie dát musí byť povolené v Nastavenia - Nastavenie komunikácie medzi aplikáciami - Prijímať Glykémie/Ošetrenia Povoliť odosielanie do xDrip+. + Samsung Tizen + TIZ + Odosielanie dát do Samsung aplikácie G-Watch Wear App (Tizen OS) Garmin Pripájanie k zariadeniu Garmin (Fénix, Edge, ...) diff --git a/pump/omnipod-common/src/main/res/values-ro-rRO/strings.xml b/pump/omnipod-common/src/main/res/values-ro-rRO/strings.xml index 08443c0d22..ad03df4912 100644 --- a/pump/omnipod-common/src/main/res/values-ro-rRO/strings.xml +++ b/pump/omnipod-common/src/main/res/values-ro-rRO/strings.xml @@ -125,6 +125,7 @@ Setare în desfășurare (se așteaptă activarea Pod-ului) Inițializarea este în curs (se așteaptă inserarea canulei) Rulează + Normal Suspendat Defecțiune Pod Timp de activare depăşit