diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.kt b/app/src/main/java/info/nightscout/androidaps/MainApp.kt index a0c49d126b..147a60cf94 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.kt +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.kt @@ -180,7 +180,7 @@ class MainApp : DaggerApplication() { Thread.currentThread().uncaughtExceptionHandler?.uncaughtException(Thread.currentThread(), e) return@setErrorHandler } - aapsLogger.warn(LTag.CORE, "Undeliverable exception received, not sure what to do", e.toString()) + aapsLogger.warn(LTag.CORE, "Undeliverable exception received, not sure what to do", e.localizedMessage) } } diff --git a/core/graph/src/main/java/info/nightscout/core/graph/OverviewData.kt b/core/graph/src/main/java/info/nightscout/core/graph/OverviewData.kt index ae6b753751..9a1eea13ba 100644 --- a/core/graph/src/main/java/info/nightscout/core/graph/OverviewData.kt +++ b/core/graph/src/main/java/info/nightscout/core/graph/OverviewData.kt @@ -25,9 +25,9 @@ import info.nightscout.interfaces.iob.IobTotal interface OverviewData { var rangeToDisplay: Int // for graph - var toTime: Long - var fromTime: Long - var endTime: Long + var toTime: Long // current time rounded up to 1 hour + var fromTime: Long // toTime - range + var endTime: Long // toTime + predictions fun reset() fun initRange() diff --git a/core/ns-sdk/src/main/java/info/nightscout/sdk/NSAndroidClientImpl.kt b/core/ns-sdk/src/main/java/info/nightscout/sdk/NSAndroidClientImpl.kt index 961d7429a8..16bdba660a 100644 --- a/core/ns-sdk/src/main/java/info/nightscout/sdk/NSAndroidClientImpl.kt +++ b/core/ns-sdk/src/main/java/info/nightscout/sdk/NSAndroidClientImpl.kt @@ -1,6 +1,7 @@ package info.nightscout.sdk import android.content.Context +import com.google.gson.JsonParser import info.nightscout.sdk.exceptions.DateHeaderOutOfToleranceException import info.nightscout.sdk.exceptions.InvalidAccessTokenException import info.nightscout.sdk.exceptions.InvalidFormatNightscoutException @@ -30,6 +31,7 @@ import info.nightscout.sdk.utils.toNotNull import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.json.JSONObject /** * @@ -190,6 +192,14 @@ class NSAndroidClientImpl( deduplicatedIdentifier = null, lastModified = null ) + } else if (response.code() == 404) { // not found + return@callWrapper CreateUpdateResponse( + response = response.code(), + identifier = null, + isDeduplication = false, + deduplicatedIdentifier = null, + lastModified = null + ) } else { throw UnsuccessfullNightscoutException() } @@ -307,6 +317,14 @@ class NSAndroidClientImpl( deduplicatedIdentifier = null, lastModified = null ) + } else if (response.code() == 404) { // not found + return@callWrapper CreateUpdateResponse( + response = response.code(), + identifier = null, + isDeduplication = false, + deduplicatedIdentifier = null, + lastModified = null + ) } else { throw UnsuccessfullNightscoutException() } @@ -375,11 +393,58 @@ class NSAndroidClientImpl( deduplicatedIdentifier = null, lastModified = null ) + } else if (response.code() == 404) { // not found + return@callWrapper CreateUpdateResponse( + response = response.code(), + identifier = null, + isDeduplication = false, + deduplicatedIdentifier = null, + lastModified = null + ) } else { throw UnsuccessfullNightscoutException() } } + override suspend fun createProfileStore(remoteProfileStore: JSONObject): CreateUpdateResponse = callWrapper(dispatcher) { + remoteProfileStore.put("app", "AAPS") + val response = api.createProfile(JsonParser.parseString(remoteProfileStore.toString()).asJsonObject) + if (response.isSuccessful) { + if (response.code() == 200) { + return@callWrapper CreateUpdateResponse( + response = response.code(), + identifier = null, + isDeduplication = true, + deduplicatedIdentifier = null, + lastModified = null + ) + } else if (response.code() == 201) { + return@callWrapper CreateUpdateResponse( + response = response.code(), + identifier = response.body()?.result?.identifier, + isDeduplication = response.body()?.result?.isDeduplication ?: false, + deduplicatedIdentifier = response.body()?.result?.deduplicatedIdentifier, + lastModified = response.body()?.result?.lastModified + ) + } else throw UnsuccessfullNightscoutException() + } else { + throw UnsuccessfullNightscoutException() + } + } + + override suspend fun getLastProfileStore(): NSAndroidClient.ReadResponse> = callWrapper(dispatcher) { + + val response = api.getLastProfile() + if (response.isSuccessful) { + val eTagString = response.headers()["ETag"] + val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() + return@callWrapper NSAndroidClient.ReadResponse(eTag, response.body()?.result.toNotNull()) + } else { + throw UnsuccessfullNightscoutException() + } + } + + private suspend fun callWrapper(dispatcher: CoroutineDispatcher, block: suspend () -> T): T = withContext(dispatcher) { retry( diff --git a/core/ns-sdk/src/main/java/info/nightscout/sdk/interfaces/NSAndroidClient.kt b/core/ns-sdk/src/main/java/info/nightscout/sdk/interfaces/NSAndroidClient.kt index eedc970bdf..2cb8bf3768 100644 --- a/core/ns-sdk/src/main/java/info/nightscout/sdk/interfaces/NSAndroidClient.kt +++ b/core/ns-sdk/src/main/java/info/nightscout/sdk/interfaces/NSAndroidClient.kt @@ -7,6 +7,7 @@ import info.nightscout.sdk.localmodel.treatment.CreateUpdateResponse import info.nightscout.sdk.localmodel.treatment.NSTreatment import info.nightscout.sdk.remotemodel.LastModified import info.nightscout.sdk.remotemodel.RemoteDeviceStatus +import org.json.JSONObject interface NSAndroidClient { @@ -32,6 +33,9 @@ interface NSAndroidClient { suspend fun createDeviceStatus(remoteDeviceStatus: RemoteDeviceStatus): CreateUpdateResponse suspend fun getDeviceStatusModifiedSince(from: Long): List + suspend fun createProfileStore(remoteProfileStore: JSONObject): CreateUpdateResponse + suspend fun getLastProfileStore(): ReadResponse> + suspend fun createTreatment(nsTreatment: NSTreatment): CreateUpdateResponse suspend fun updateTreatment(nsTreatment: NSTreatment): CreateUpdateResponse suspend fun getFoods(limit: Long): List diff --git a/core/ns-sdk/src/main/java/info/nightscout/sdk/networking/NetworkStackBuilder.kt b/core/ns-sdk/src/main/java/info/nightscout/sdk/networking/NetworkStackBuilder.kt index 87d7e6ef4a..53facf6216 100644 --- a/core/ns-sdk/src/main/java/info/nightscout/sdk/networking/NetworkStackBuilder.kt +++ b/core/ns-sdk/src/main/java/info/nightscout/sdk/networking/NetworkStackBuilder.kt @@ -3,9 +3,11 @@ package info.nightscout.sdk.networking import android.content.Context import com.google.gson.Gson import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializer import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor +import org.json.JSONObject import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit @@ -85,7 +87,13 @@ internal object NetworkStackBuilder { return build() } - private fun provideGson(): Gson = GsonBuilder().create() + private val deserializer: JsonDeserializer = + JsonDeserializer { json, _, _ -> + JSONObject(json.asJsonObject.toString()) + } + private fun provideGson(): Gson = GsonBuilder().also { + it.registerTypeAdapter(JSONObject::class.java, deserializer) + }.create() private const val OK_HTTP_CACHE_SIZE = 10L * 1024 * 1024 private const val OK_HTTP_READ_TIMEOUT = 60L * 1000 diff --git a/core/ns-sdk/src/main/java/info/nightscout/sdk/networking/NightscoutRemoteService.kt b/core/ns-sdk/src/main/java/info/nightscout/sdk/networking/NightscoutRemoteService.kt index 824aada95e..59af887bdd 100644 --- a/core/ns-sdk/src/main/java/info/nightscout/sdk/networking/NightscoutRemoteService.kt +++ b/core/ns-sdk/src/main/java/info/nightscout/sdk/networking/NightscoutRemoteService.kt @@ -1,5 +1,6 @@ package info.nightscout.sdk.networking +import com.google.gson.JsonObject import info.nightscout.sdk.remotemodel.LastModified import info.nightscout.sdk.remotemodel.NSResponse import info.nightscout.sdk.remotemodel.RemoteCreateUpdateResponse @@ -8,6 +9,7 @@ import info.nightscout.sdk.remotemodel.RemoteEntry import info.nightscout.sdk.remotemodel.RemoteFood import info.nightscout.sdk.remotemodel.RemoteStatusResponse import info.nightscout.sdk.remotemodel.RemoteTreatment +import org.json.JSONObject import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET @@ -70,14 +72,21 @@ internal interface NightscoutRemoteService { @GET("v3/food") suspend fun getFoods(@Query("limit") limit: Long): Response>> -/* - @GET("v3/food/history/{from}") - suspend fun getFoodsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Long): Response>> -*/ + + /* + @GET("v3/food/history/{from}") + suspend fun getFoodsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Long): Response>> + */ @POST("v3/food") suspend fun createFood(@Body remoteFood: RemoteFood): Response> @PATCH("v3/food") suspend fun updateFood(@Body remoteFood: RemoteFood): Response> + @GET("v3/profile?sort\$desc=date&limit=1") + suspend fun getLastProfile(): Response>> + + @POST("v3/profile") + suspend fun createProfile(@Body profile: JsonObject): Response> + } diff --git a/core/ns-sdk/src/main/java/info/nightscout/sdk/remotemodel/LastModified.kt b/core/ns-sdk/src/main/java/info/nightscout/sdk/remotemodel/LastModified.kt index 450760ae3a..2a9716adc6 100644 --- a/core/ns-sdk/src/main/java/info/nightscout/sdk/remotemodel/LastModified.kt +++ b/core/ns-sdk/src/main/java/info/nightscout/sdk/remotemodel/LastModified.kt @@ -19,6 +19,7 @@ data class LastModified( @SerializedName("entries") var entries: Long = 0, // entries collection @SerializedName("profile") var profile: Long = 0, // profile collection @SerializedName("treatments") var treatments: Long = 0, // treatments collection - @SerializedName("foods") var foods: Long = 0 // foods collection + @SerializedName("foods") var foods: Long = 0, // foods collection + @SerializedName("settings") var settings: Long = 0 // settings collection ) } diff --git a/core/ns-sdk/src/main/java/info/nightscout/sdk/remotemodel/RemoteProfileStore.kt b/core/ns-sdk/src/main/java/info/nightscout/sdk/remotemodel/RemoteProfileStore.kt new file mode 100644 index 0000000000..4f2c0289d5 --- /dev/null +++ b/core/ns-sdk/src/main/java/info/nightscout/sdk/remotemodel/RemoteProfileStore.kt @@ -0,0 +1,49 @@ +package info.nightscout.sdk.remotemodel + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import org.json.JSONObject + +/** + * DeviceStatus coming from uploader or AAPS + * + **/ +@Serializable +data class RemoteProfileStore( + @SerializedName("app") var app: String? = null, + @SerializedName("identifier") val identifier: String? = null, // string Main addressing, required field that identifies document in the collection. The client should not create the identifier, the server automatically assigns it when the document is inserted. + @SerializedName("srvCreated") val srvCreated: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of document insertion into the database (Unix epoch in ms). This field appears only for documents which were inserted by API v3. + @SerializedName("srvModified") val srvModified: Long? = null, // integer($int64) example: 1525383610088 The server's timestamp of the last document modification in the database (Unix epoch in ms). This field appears only for documents which were somehow modified by API v3 (inserted, updated or deleted). + @SerializedName("created_at") val createdAt: String? = null, // string or string timestamp on previous version of api, in my examples, a lot of treatments don't have date, only created_at, some of them with string others with long... + @SerializedName("date") val date: Long?, // date as milliseconds + @SerializedName("startDate") val startDate: Long?, // record valid from + @SerializedName("defaultProfile") val defaultProfile: String,// default profile in store + + //@Serializable(with = JSONSerializer::class) + @Contextual @SerializedName("store") val store: JSONObject +) { +/* + @Serializable data class Store( + val names: ArrayList, + val profiles: ArrayList + ) + + @Serializable data class SimpleProfile( + @SerializedName("dia") val dia: Double, + @SerializedName("carbratio") val carbratio: ArrayList, + @SerializedName("sens") val sens: ArrayList, + @SerializedName("basal") val basal: ArrayList, + @SerializedName("target_low") val target_low: ArrayList, + @SerializedName("target_high") val target_high: ArrayList, + @SerializedName("units") val units: String, // string The units for the glucose value, mg/dl or mmoll + @SerializedName("timezone") val timezone: String + ) + + @Serializable data class ProfileEntry( + @SerializedName("time") val time: String, + @SerializedName("timeAsSeconds") val timeAsSeconds: Long?, + @SerializedName("value") val value: Double + ) +*/ +} \ No newline at end of file diff --git a/plugins/main/src/main/java/info/nightscout/plugins/profile/ProfilePlugin.kt b/plugins/main/src/main/java/info/nightscout/plugins/profile/ProfilePlugin.kt index bbcec9b28b..58c0b39804 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/profile/ProfilePlugin.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/profile/ProfilePlugin.kt @@ -22,9 +22,9 @@ import info.nightscout.interfaces.plugin.ActivePlugin import info.nightscout.interfaces.plugin.PluginBase import info.nightscout.interfaces.plugin.PluginDescription import info.nightscout.interfaces.plugin.PluginType +import info.nightscout.interfaces.profile.Instantiator import info.nightscout.interfaces.profile.Profile import info.nightscout.interfaces.profile.ProfileFunction -import info.nightscout.interfaces.profile.Instantiator import info.nightscout.interfaces.profile.ProfileSource import info.nightscout.interfaces.profile.ProfileStore import info.nightscout.interfaces.profile.PureProfile @@ -413,6 +413,7 @@ class ProfilePlugin @Inject constructor( } if (numOfProfiles > 0) json.put("defaultProfile", currentProfile()?.name) val startDate = sp.getLong(info.nightscout.core.utils.R.string.key_local_profile_last_change, dateUtil.now()) + json.put("date", startDate) json.put("startDate", dateUtil.toISOAsUTC(startDate)) json.put("store", store) } catch (e: JSONException) { diff --git a/plugins/sync/src/main/java/info/nightscout/plugins/sync/di/SyncModule.kt b/plugins/sync/src/main/java/info/nightscout/plugins/sync/di/SyncModule.kt index 61cf0a2327..1392dead5d 100644 --- a/plugins/sync/src/main/java/info/nightscout/plugins/sync/di/SyncModule.kt +++ b/plugins/sync/src/main/java/info/nightscout/plugins/sync/di/SyncModule.kt @@ -22,6 +22,7 @@ import info.nightscout.plugins.sync.nsclientV3.workers.LoadBgWorker import info.nightscout.plugins.sync.nsclientV3.workers.LoadDeviceStatusWorker import info.nightscout.plugins.sync.nsclientV3.workers.LoadFoodsWorker import info.nightscout.plugins.sync.nsclientV3.workers.LoadLastModificationWorker +import info.nightscout.plugins.sync.nsclientV3.workers.LoadProfileStoreWorker import info.nightscout.plugins.sync.nsclientV3.workers.LoadStatusWorker import info.nightscout.plugins.sync.nsclientV3.workers.LoadTreatmentsWorker import info.nightscout.plugins.sync.nsclientV3.workers.ProcessFoodWorker @@ -49,6 +50,7 @@ abstract class SyncModule { @ContributesAndroidInjector abstract fun contributesLoadLastModificationWorker(): LoadLastModificationWorker @ContributesAndroidInjector abstract fun contributesLoadBgWorker(): LoadBgWorker @ContributesAndroidInjector abstract fun contributesLoadFoodsWorker(): LoadFoodsWorker + @ContributesAndroidInjector abstract fun contributesLoadProfileStoreWorker(): LoadProfileStoreWorker @ContributesAndroidInjector abstract fun contributesStoreBgWorker(): StoreDataForDbImpl.StoreBgWorker @ContributesAndroidInjector abstract fun contributesStoreFoodWorker(): StoreDataForDbImpl.StoreFoodWorker @ContributesAndroidInjector abstract fun contributesTreatmentWorker(): LoadTreatmentsWorker diff --git a/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsShared/DataSyncSelectorImplementation.kt b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsShared/DataSyncSelectorImplementation.kt index a0ec25f72d..60562c323c 100644 --- a/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsShared/DataSyncSelectorImplementation.kt +++ b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsShared/DataSyncSelectorImplementation.kt @@ -5,6 +5,7 @@ import info.nightscout.database.impl.AppRepository import info.nightscout.interfaces.plugin.ActivePlugin import info.nightscout.interfaces.profile.ProfileFunction import info.nightscout.interfaces.sync.DataSyncSelector +import info.nightscout.interfaces.utils.JsonHelper import info.nightscout.plugins.sync.R import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag @@ -764,7 +765,11 @@ class DataSyncSelectorImplementation @Inject constructor( if (lastChange == 0L) return if (lastChange > lastSync) { if (activePlugin.activeProfileSource.profile?.allProfilesValid != true) return - val profileJson = activePlugin.activeProfileSource.profile?.data ?: return + val profileStore = activePlugin.activeProfileSource.profile + val profileJson = profileStore?.data ?: return + // add for v3 + if (JsonHelper.safeGetLongAllowNull(profileJson, "date") == null) + profileJson.put("date", profileStore.getStartDate()) activePlugin.activeNsClient?.nsAdd("profile", DataSyncSelector.PairProfileStore(profileJson, dateUtil.now()), "") } } diff --git a/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/NSClientV3Plugin.kt b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/NSClientV3Plugin.kt index 9ef50f4fa2..4c1f366177 100644 --- a/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/NSClientV3Plugin.kt +++ b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/NSClientV3Plugin.kt @@ -16,11 +16,12 @@ import com.google.gson.GsonBuilder import dagger.android.HasAndroidInjector import info.nightscout.core.utils.fabric.FabricPrivacy import info.nightscout.core.validators.ValidatingEditTextPreference +import info.nightscout.database.ValueWrapper import info.nightscout.database.entities.interfaces.TraceableDBEntry +import info.nightscout.database.impl.AppRepository import info.nightscout.interfaces.Config import info.nightscout.interfaces.Constants import info.nightscout.interfaces.nsclient.NSAlarm -import info.nightscout.interfaces.nsclient.StoreDataForDb import info.nightscout.interfaces.plugin.PluginBase import info.nightscout.interfaces.plugin.PluginDescription import info.nightscout.interfaces.plugin.PluginType @@ -98,9 +99,9 @@ class NSClientV3Plugin @Inject constructor( private val config: Config, private val dateUtil: DateUtil, private val uiInteraction: UiInteraction, - private val storeDataForDb: StoreDataForDb, private val dataSyncSelector: DataSyncSelector, - private val profileFunction: ProfileFunction + private val profileFunction: ProfileFunction, + private val repository: AppRepository ) : NsClient, Sync, PluginBase( PluginDescription() .mainType(PluginType.SYNC) @@ -116,7 +117,7 @@ class NSClientV3Plugin @Inject constructor( companion object { val JOB_NAME: String = this::class.java.simpleName - val REFRESH_INTERVAL = T.mins(5).msecs() + val REFRESH_INTERVAL = T.secs(30).msecs() } private val disposable = CompositeDisposable() @@ -196,18 +197,26 @@ class NSClientV3Plugin @Inject constructor( disposable += rxBus .toObservable(EventNewBG::class.java) .observeOn(aapsSchedulers.io) - .subscribe({ scheduleExecution("NEW_BG") }, fabricPrivacy::logException) + .subscribe({ delayAndScheduleExecution("NEW_BG") }, fabricPrivacy::logException) disposable += rxBus .toObservable(EventNewHistoryData::class.java) .observeOn(aapsSchedulers.io) - .subscribe({ scheduleExecution("NEW_DATA") }, fabricPrivacy::logException) + .subscribe({ delayAndScheduleExecution("NEW_DATA") }, fabricPrivacy::logException) runLoop = Runnable { handler.postDelayed(runLoop, REFRESH_INTERVAL) - executeLoop("MAIN_LOOP") + repository.getLastGlucoseValueWrapped().blockingGet().let { + // if last value is older than 5 min or there is no bg + if (it is ValueWrapper.Existing) { + if (it.value.timestamp < dateUtil.now() - T.mins(5).plus(T.secs(20)).msecs()) + executeLoop("MAIN_LOOP", forceNew = false) + else + rxBus.send(EventNSClientNewLog("RECENT", "No need to load")) + } else executeLoop("MAIN_LOOP", forceNew = false) + } } handler.postDelayed(runLoop, REFRESH_INTERVAL) - executeLoop("START") + executeLoop("START", forceNew = false) } override fun onStop() { @@ -273,7 +282,7 @@ class NSClientV3Plugin @Inject constructor( } override fun resend(reason: String) { - executeLoop("RESEND") + executeLoop("RESEND", forceNew = false) } override fun pause(newState: Boolean) { @@ -331,6 +340,36 @@ class NSClientV3Plugin @Inject constructor( enum class Operation { CREATE, UPDATE } private val gson: Gson = GsonBuilder().create() + private fun dbOperationProfileStore(collection: String = "profile", dataPair: DataSyncSelector.DataPair, progress: String) { + val data = (dataPair as DataSyncSelector.PairProfileStore).value + scope.launch { + try { + rxBus.send(EventNSClientNewLog("ADD $collection", "Sent ${dataPair.javaClass.simpleName} $data $progress")) + nsAndroidClient?.createProfileStore(data)?.let { result -> + when (result.response) { + 200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ProfileStore")) + 201 -> rxBus.send(EventNSClientNewLog("ADDED", "OK ProfileStore")) + 404 -> rxBus.send(EventNSClientNewLog("NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}")) + + else -> { + rxBus.send(EventNSClientNewLog("ERROR", "ProfileStore")) + return@launch + } + } + // if (result.response == 201) { // created + // dataPair.value.interfaceIDs.nightscoutId = result.identifier + // storeDataForDb.nsIdDeviceStatuses.add(dataPair.value) + // storeDataForDb.scheduleNsIdUpdate() + // } + dataSyncSelector.confirmLastProfileStore(dataPair.id) + dataSyncSelector.processChangedProfileStore() + } + } catch (e: Exception) { + aapsLogger.error(LTag.NSCLIENT, "Upload exception", e) + } + } + } + private fun dbOperationDeviceStatus(collection: String = "devicestatus", dataPair: DataSyncSelector.DataPair, progress: String) { val data = (dataPair as DataSyncSelector.PairDeviceStatus).value.toRemoteDeviceStatus() scope.launch { @@ -340,6 +379,7 @@ class NSClientV3Plugin @Inject constructor( when (result.response) { 200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ${dataPair.value.javaClass.simpleName}")) 201 -> rxBus.send(EventNSClientNewLog("ADDED", "OK ${dataPair.value.javaClass.simpleName} ${result.identifier}")) + 404 -> rxBus.send(EventNSClientNewLog("NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}")) else -> { rxBus.send(EventNSClientNewLog("ERROR", "${dataPair.value.javaClass.simpleName} ")) @@ -389,6 +429,7 @@ class NSClientV3Plugin @Inject constructor( 200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ${dataPair.value.javaClass.simpleName}")) 201 -> rxBus.send(EventNSClientNewLog("ADDED", "OK ${dataPair.value.javaClass.simpleName}")) 400 -> rxBus.send(EventNSClientNewLog("FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}")) + 404 -> rxBus.send(EventNSClientNewLog("NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}")) else -> { rxBus.send(EventNSClientNewLog("ERROR", "${dataPair.value.javaClass.simpleName} ")) @@ -443,6 +484,7 @@ class NSClientV3Plugin @Inject constructor( 200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ${dataPair.value.javaClass.simpleName}")) 201 -> rxBus.send(EventNSClientNewLog("ADDED", "OK ${dataPair.value.javaClass.simpleName}")) 400 -> rxBus.send(EventNSClientNewLog("FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}")) + 404 -> rxBus.send(EventNSClientNewLog("NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}")) else -> { rxBus.send(EventNSClientNewLog("ERROR", "${dataPair.value.javaClass.simpleName} ")) @@ -525,6 +567,7 @@ class NSClientV3Plugin @Inject constructor( 200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ${dataPair.value.javaClass.simpleName}")) 201 -> rxBus.send(EventNSClientNewLog("ADDED", "OK ${dataPair.value.javaClass.simpleName}")) 400 -> rxBus.send(EventNSClientNewLog("FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}")) + 404 -> rxBus.send(EventNSClientNewLog("NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}")) else -> { rxBus.send(EventNSClientNewLog("ERROR", "${dataPair.value.javaClass.simpleName} ")) @@ -642,6 +685,7 @@ class NSClientV3Plugin @Inject constructor( private fun dbOperation(collection: String, dataPair: DataSyncSelector.DataPair, progress: String, operation: Operation) { when (collection) { + "profile" -> dbOperationProfileStore(dataPair = dataPair, progress = progress) "devicestatus" -> dbOperationDeviceStatus(dataPair = dataPair, progress = progress) "entries" -> dbOperationEntries(dataPair = dataPair, progress = progress, operation = operation) "food" -> dbOperationFood(dataPair = dataPair, progress = progress, operation = operation) @@ -653,18 +697,20 @@ class NSClientV3Plugin @Inject constructor( sp.putString(R.string.key_ns_client_v3_last_modified, Json.encodeToString(LastModified.serializer(), lastLoadedSrvModified)) } - fun scheduleNewExecution() { + fun scheduleIrregularExecution() { var origin = "5_MIN_AFTER_BG" + var forceNew = true var toTime = lastLoadedSrvModified.collections.entries + T.mins(5).plus(T.secs(10)).msecs() if (toTime < dateUtil.now()) { toTime = dateUtil.now() + T.mins(1).plus(T.secs(0)).msecs() origin = "1_MIN_OLD_DATA" + forceNew = false } - handler.postDelayed({ executeLoop(origin) }, toTime - dateUtil.now()) + handler.postDelayed({ executeLoop(origin, forceNew = forceNew) }, toTime - dateUtil.now()) rxBus.send(EventNSClientNewLog("NEXT", dateUtil.dateAndTimeAndSecondsString(toTime))) } - private fun executeLoop(origin: String) { + private fun executeLoop(origin: String, forceNew: Boolean) { if (sp.getBoolean(R.string.key_ns_client_paused, false)) { rxBus.send(EventNSClientNewLog("RUN", "paused")) return @@ -673,25 +719,28 @@ class NSClientV3Plugin @Inject constructor( rxBus.send(EventNSClientNewLog("RUN", blockingReason)) return } - if (workIsRunning(arrayOf(JOB_NAME))) + if (workIsRunning(arrayOf(JOB_NAME))) { rxBus.send(EventNSClientNewLog("RUN", "Already running $origin")) - else { - rxBus.send(EventNSClientNewLog("RUN", "Starting next round $origin")) - WorkManager.getInstance(context) - .beginUniqueWork( - "NSCv3Load", - ExistingWorkPolicy.REPLACE, - OneTimeWorkRequest.Builder(LoadStatusWorker::class.java).build() - ) - .then(OneTimeWorkRequest.Builder(LoadLastModificationWorker::class.java).build()) - .then(OneTimeWorkRequest.Builder(LoadBgWorker::class.java).build()) - // Other Workers are enqueued after BG finish - // LoadTreatmentsWorker - // LoadFoodsWorker - // LoadDeviceStatusWorker - // DataSyncWorker - .enqueue() + if (!forceNew) return + // Wait for end and start new cycle + while (workIsRunning(arrayOf(JOB_NAME))) Thread.sleep(5000) } + rxBus.send(EventNSClientNewLog("RUN", "Starting next round $origin")) + WorkManager.getInstance(context) + .beginUniqueWork( + JOB_NAME, + ExistingWorkPolicy.REPLACE, + OneTimeWorkRequest.Builder(LoadStatusWorker::class.java).build() + ) + .then(OneTimeWorkRequest.Builder(LoadLastModificationWorker::class.java).build()) + .then(OneTimeWorkRequest.Builder(LoadBgWorker::class.java).build()) + // Other Workers are enqueued after BG finish + // LoadTreatmentsWorker + // LoadFoodsWorker + // LoadProfileStoreWorker + // LoadDeviceStatusWorker + // DataSyncWorker + .enqueue() } private fun workIsRunning(workNames: Array): Boolean { @@ -704,12 +753,12 @@ class NSClientV3Plugin @Inject constructor( private val eventWorker = Executors.newSingleThreadScheduledExecutor() private var scheduledEventPost: ScheduledFuture<*>? = null - private fun scheduleExecution(origin: String) { + private fun delayAndScheduleExecution(origin: String) { class PostRunnable : Runnable { override fun run() { scheduledEventPost = null - executeLoop(origin) + executeLoop(origin, forceNew = true) } } // cancel waiting task to prevent sending multiple posts diff --git a/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadBgWorker.kt b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadBgWorker.kt index a5f14ab119..c295ae177e 100644 --- a/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadBgWorker.kt +++ b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadBgWorker.kt @@ -57,7 +57,7 @@ class LoadBgWorker( sgvs = response.values response.lastServerModified?.let { nsClientV3Plugin.lastLoadedSrvModified.collections.entries = it } nsClientV3Plugin.storeLastLoadedSrvModified() - nsClientV3Plugin.scheduleNewExecution() // Idea is to run after 5 min after last BG + nsClientV3Plugin.scheduleIrregularExecution() // Idea is to run after 5 min after last BG } aapsLogger.debug("SGVS: $sgvs") if (sgvs.isNotEmpty()) { @@ -77,8 +77,7 @@ class LoadBgWorker( nsClientV3Plugin.lastLoadedSrvModified.collections.entries = lastLoaded nsClientV3Plugin.storeLastLoadedSrvModified() } - rxBus.send(EventNSClientNewLog("RCV END", "No SGVs from ${dateUtil - .dateAndTimeAndSecondsString(lastLoaded)}")) + rxBus.send(EventNSClientNewLog("RCV END", "No SGVs from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}")) WorkManager.getInstance(context) .beginUniqueWork( NSClientV3Plugin.JOB_NAME, diff --git a/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadFoodsWorker.kt b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadFoodsWorker.kt index 3daca3a700..52c90df8aa 100644 --- a/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadFoodsWorker.kt +++ b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadFoodsWorker.kt @@ -9,7 +9,6 @@ import androidx.work.workDataOf import info.nightscout.core.utils.receivers.DataWorkerStorage import info.nightscout.core.utils.worker.LoggingWorker import info.nightscout.interfaces.nsclient.StoreDataForDb -import info.nightscout.interfaces.workflow.WorkerClasses import info.nightscout.plugins.sync.nsShared.StoreDataForDbImpl import info.nightscout.plugins.sync.nsclientV3.NSClientV3Plugin import info.nightscout.rx.bus.RxBus @@ -30,7 +29,6 @@ class LoadFoodsWorker( @Inject lateinit var nsClientV3Plugin: NSClientV3Plugin @Inject lateinit var dateUtil: DateUtil @Inject lateinit var storeDataForDb: StoreDataForDb - @Inject lateinit var workerClasses: WorkerClasses override fun doWorkAndLog(): Result { val nsAndroidClient = nsClientV3Plugin.nsAndroidClient ?: return Result.failure(workDataOf("Error" to "AndroidClient is null")) @@ -51,7 +49,7 @@ class LoadFoodsWorker( .setInputData(dataWorkerStorage.storeInputData(foods)) .build() ).then(OneTimeWorkRequest.Builder(StoreDataForDbImpl.StoreFoodWorker::class.java).build()) - .then(OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build()) + .then(OneTimeWorkRequest.Builder(LoadProfileStoreWorker::class.java).build()) .enqueue() } else { rxBus.send(EventNSClientNewLog("RCV", "FOOD skipped")) @@ -59,7 +57,7 @@ class LoadFoodsWorker( .enqueueUniqueWork( NSClientV3Plugin.JOB_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, - OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build() + OneTimeWorkRequest.Builder(LoadProfileStoreWorker::class.java).build() ) } } diff --git a/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadProfileStoreWorker.kt b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadProfileStoreWorker.kt new file mode 100644 index 0000000000..fc4e8462d1 --- /dev/null +++ b/plugins/sync/src/main/java/info/nightscout/plugins/sync/nsclientV3/workers/LoadProfileStoreWorker.kt @@ -0,0 +1,85 @@ +package info.nightscout.plugins.sync.nsclientV3.workers + +import android.content.Context +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import info.nightscout.core.utils.receivers.DataWorkerStorage +import info.nightscout.core.utils.worker.LoggingWorker +import info.nightscout.interfaces.utils.JsonHelper +import info.nightscout.interfaces.workflow.WorkerClasses +import info.nightscout.plugins.sync.nsclientV3.NSClientV3Plugin +import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventNSClientNewLog +import info.nightscout.sdk.interfaces.NSAndroidClient +import info.nightscout.shared.utils.DateUtil +import kotlinx.coroutines.runBlocking +import org.json.JSONObject +import javax.inject.Inject +import kotlin.math.max + +class LoadProfileStoreWorker( + context: Context, + params: WorkerParameters +) : LoggingWorker(context, params) { + + @Inject lateinit var dataWorkerStorage: DataWorkerStorage + @Inject lateinit var rxBus: RxBus + @Inject lateinit var context: Context + @Inject lateinit var nsClientV3Plugin: NSClientV3Plugin + @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var workerClasses: WorkerClasses + + override fun doWorkAndLog(): Result { + val nsAndroidClient = nsClientV3Plugin.nsAndroidClient ?: return Result.failure(workDataOf("Error" to "AndroidClient is null")) + var ret = Result.success() + + val lastLoaded = max(nsClientV3Plugin.lastLoadedSrvModified.collections.profile, 0) + runBlocking { + if ((nsClientV3Plugin.newestDataOnServer?.collections?.profile ?: Long.MAX_VALUE) > lastLoaded) + try { + val response: NSAndroidClient.ReadResponse> = nsAndroidClient.getLastProfileStore() + val profiles = response.values + if (profiles.size == 1) { + val profile = profiles[0] + JsonHelper.safeGetLongAllowNull(profile, "srvModified")?.let { nsClientV3Plugin.lastLoadedSrvModified.collections.profile = it } + nsClientV3Plugin.storeLastLoadedSrvModified() + aapsLogger.debug("PROFILE: $profile") + rxBus.send(EventNSClientNewLog("RCV", "1 PROFILE from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}")) + WorkManager.getInstance(context) + .beginUniqueWork( + NSClientV3Plugin.JOB_NAME, + ExistingWorkPolicy.APPEND_OR_REPLACE, + OneTimeWorkRequest.Builder((workerClasses.nsProfileWorker)) + .setInputData(dataWorkerStorage.storeInputData(profile)) + .build() + ).then(OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build()) + .enqueue() + } else { + rxBus.send(EventNSClientNewLog("RCV END", "No new PROFILE from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}")) + WorkManager.getInstance(context) + .enqueueUniqueWork( + NSClientV3Plugin.JOB_NAME, + ExistingWorkPolicy.APPEND_OR_REPLACE, + OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build() + ) + } + } catch (error: Exception) { + aapsLogger.error("Error: ", error) + ret = Result.failure(workDataOf("Error" to error.toString())) + } + else { + rxBus.send(EventNSClientNewLog("RCV END", "No PROFILE from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}")) + WorkManager.getInstance(context) + .enqueueUniqueWork( + NSClientV3Plugin.JOB_NAME, + ExistingWorkPolicy.APPEND_OR_REPLACE, + OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build() + ) + } + } + return ret + } +} \ No newline at end of file diff --git a/plugins/sync/src/test/java/info/nightscout/plugins/sync/nsclientV3/NSClientV3PluginTest.kt b/plugins/sync/src/test/java/info/nightscout/plugins/sync/nsclientV3/NSClientV3PluginTest.kt index d6b5e995c8..b6bd808f5f 100644 --- a/plugins/sync/src/test/java/info/nightscout/plugins/sync/nsclientV3/NSClientV3PluginTest.kt +++ b/plugins/sync/src/test/java/info/nightscout/plugins/sync/nsclientV3/NSClientV3PluginTest.kt @@ -69,7 +69,7 @@ internal class NSClientV3PluginTest : TestBaseWithProfile() { sut = NSClientV3Plugin( injector, aapsLogger, aapsSchedulers, rxBus, rh, context, fabricPrivacy, - sp, nsClientReceiverDelegate, config, dateUtil, uiInteraction, storeDataForDb, dataSyncSelector, mockedProfileFunction + sp, nsClientReceiverDelegate, config, dateUtil, uiInteraction, dataSyncSelector, mockedProfileFunction, repository ) sut.nsAndroidClient = nsAndroidClient `when`(mockedProfileFunction.getProfile(anyLong())).thenReturn(validProfile) @@ -519,4 +519,22 @@ internal class NSClientV3PluginTest : TestBaseWithProfile() { verify(dataSyncSelector, Times(2)).confirmLastTherapyEventIdIfGreater(1000) verify(dataSyncSelector, Times(2)).processChangedTherapyEvents() } + + @Test + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + fun nsAddProfile() = runTest { + sut.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler)) + + val dataPair = DataSyncSelector.PairProfileStore(getValidProfileStore().data, 1000) + // create + `when`(nsAndroidClient.createProfileStore(anyObject())).thenReturn(CreateUpdateResponse(201, null)) + sut.nsAdd("profile", dataPair, "1/3") + verify(dataSyncSelector, Times(1)).confirmLastProfileStore(1000) + verify(dataSyncSelector, Times(1)).processChangedProfileStore() + // update + `when`(nsAndroidClient.updateTreatment(anyObject())).thenReturn(CreateUpdateResponse(200, null)) + sut.nsUpdate("profile", dataPair, "1/3") + verify(dataSyncSelector, Times(2)).confirmLastProfileStore(1000) + verify(dataSyncSelector, Times(2)).processChangedProfileStore() + } } \ No newline at end of file