NSCv3: process ProfileStore

This commit is contained in:
Milos Kozak 2023-01-08 10:52:28 +01:00
parent 9412f88672
commit 3bc62e9a01
16 changed files with 344 additions and 51 deletions

View file

@ -180,7 +180,7 @@ class MainApp : DaggerApplication() {
Thread.currentThread().uncaughtExceptionHandler?.uncaughtException(Thread.currentThread(), e) Thread.currentThread().uncaughtExceptionHandler?.uncaughtException(Thread.currentThread(), e)
return@setErrorHandler 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)
} }
} }

View file

@ -25,9 +25,9 @@ import info.nightscout.interfaces.iob.IobTotal
interface OverviewData { interface OverviewData {
var rangeToDisplay: Int // for graph var rangeToDisplay: Int // for graph
var toTime: Long var toTime: Long // current time rounded up to 1 hour
var fromTime: Long var fromTime: Long // toTime - range
var endTime: Long var endTime: Long // toTime + predictions
fun reset() fun reset()
fun initRange() fun initRange()

View file

@ -1,6 +1,7 @@
package info.nightscout.sdk package info.nightscout.sdk
import android.content.Context import android.content.Context
import com.google.gson.JsonParser
import info.nightscout.sdk.exceptions.DateHeaderOutOfToleranceException import info.nightscout.sdk.exceptions.DateHeaderOutOfToleranceException
import info.nightscout.sdk.exceptions.InvalidAccessTokenException import info.nightscout.sdk.exceptions.InvalidAccessTokenException
import info.nightscout.sdk.exceptions.InvalidFormatNightscoutException import info.nightscout.sdk.exceptions.InvalidFormatNightscoutException
@ -30,6 +31,7 @@ import info.nightscout.sdk.utils.toNotNull
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONObject
/** /**
* *
@ -190,6 +192,14 @@ class NSAndroidClientImpl(
deduplicatedIdentifier = null, deduplicatedIdentifier = null,
lastModified = 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 { } else {
throw UnsuccessfullNightscoutException() throw UnsuccessfullNightscoutException()
} }
@ -307,6 +317,14 @@ class NSAndroidClientImpl(
deduplicatedIdentifier = null, deduplicatedIdentifier = null,
lastModified = 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 { } else {
throw UnsuccessfullNightscoutException() throw UnsuccessfullNightscoutException()
} }
@ -375,11 +393,58 @@ class NSAndroidClientImpl(
deduplicatedIdentifier = null, deduplicatedIdentifier = null,
lastModified = 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 { } else {
throw UnsuccessfullNightscoutException() 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<List<JSONObject>> = 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 <T> callWrapper(dispatcher: CoroutineDispatcher, block: suspend () -> T): T = private suspend fun <T> callWrapper(dispatcher: CoroutineDispatcher, block: suspend () -> T): T =
withContext(dispatcher) { withContext(dispatcher) {
retry( retry(

View file

@ -7,6 +7,7 @@ import info.nightscout.sdk.localmodel.treatment.CreateUpdateResponse
import info.nightscout.sdk.localmodel.treatment.NSTreatment import info.nightscout.sdk.localmodel.treatment.NSTreatment
import info.nightscout.sdk.remotemodel.LastModified import info.nightscout.sdk.remotemodel.LastModified
import info.nightscout.sdk.remotemodel.RemoteDeviceStatus import info.nightscout.sdk.remotemodel.RemoteDeviceStatus
import org.json.JSONObject
interface NSAndroidClient { interface NSAndroidClient {
@ -32,6 +33,9 @@ interface NSAndroidClient {
suspend fun createDeviceStatus(remoteDeviceStatus: RemoteDeviceStatus): CreateUpdateResponse suspend fun createDeviceStatus(remoteDeviceStatus: RemoteDeviceStatus): CreateUpdateResponse
suspend fun getDeviceStatusModifiedSince(from: Long): List<RemoteDeviceStatus> suspend fun getDeviceStatusModifiedSince(from: Long): List<RemoteDeviceStatus>
suspend fun createProfileStore(remoteProfileStore: JSONObject): CreateUpdateResponse
suspend fun getLastProfileStore(): ReadResponse<List<JSONObject>>
suspend fun createTreatment(nsTreatment: NSTreatment): CreateUpdateResponse suspend fun createTreatment(nsTreatment: NSTreatment): CreateUpdateResponse
suspend fun updateTreatment(nsTreatment: NSTreatment): CreateUpdateResponse suspend fun updateTreatment(nsTreatment: NSTreatment): CreateUpdateResponse
suspend fun getFoods(limit: Long): List<NSFood> suspend fun getFoods(limit: Long): List<NSFood>

View file

@ -3,9 +3,11 @@ package info.nightscout.sdk.networking
import android.content.Context import android.content.Context
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializer
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import org.json.JSONObject
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -85,7 +87,13 @@ internal object NetworkStackBuilder {
return build() return build()
} }
private fun provideGson(): Gson = GsonBuilder().create() private val deserializer: JsonDeserializer<JSONObject?> =
JsonDeserializer<JSONObject?> { 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_CACHE_SIZE = 10L * 1024 * 1024
private const val OK_HTTP_READ_TIMEOUT = 60L * 1000 private const val OK_HTTP_READ_TIMEOUT = 60L * 1000

View file

@ -1,5 +1,6 @@
package info.nightscout.sdk.networking package info.nightscout.sdk.networking
import com.google.gson.JsonObject
import info.nightscout.sdk.remotemodel.LastModified import info.nightscout.sdk.remotemodel.LastModified
import info.nightscout.sdk.remotemodel.NSResponse import info.nightscout.sdk.remotemodel.NSResponse
import info.nightscout.sdk.remotemodel.RemoteCreateUpdateResponse 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.RemoteFood
import info.nightscout.sdk.remotemodel.RemoteStatusResponse import info.nightscout.sdk.remotemodel.RemoteStatusResponse
import info.nightscout.sdk.remotemodel.RemoteTreatment import info.nightscout.sdk.remotemodel.RemoteTreatment
import org.json.JSONObject
import retrofit2.Response import retrofit2.Response
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
@ -70,14 +72,21 @@ internal interface NightscoutRemoteService {
@GET("v3/food") @GET("v3/food")
suspend fun getFoods(@Query("limit") limit: Long): Response<NSResponse<List<RemoteFood>>> suspend fun getFoods(@Query("limit") limit: Long): Response<NSResponse<List<RemoteFood>>>
/*
@GET("v3/food/history/{from}") /*
suspend fun getFoodsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Long): Response<NSResponse<List<RemoteFood>>> @GET("v3/food/history/{from}")
*/ suspend fun getFoodsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Long): Response<NSResponse<List<RemoteFood>>>
*/
@POST("v3/food") @POST("v3/food")
suspend fun createFood(@Body remoteFood: RemoteFood): Response<NSResponse<RemoteCreateUpdateResponse>> suspend fun createFood(@Body remoteFood: RemoteFood): Response<NSResponse<RemoteCreateUpdateResponse>>
@PATCH("v3/food") @PATCH("v3/food")
suspend fun updateFood(@Body remoteFood: RemoteFood): Response<NSResponse<RemoteCreateUpdateResponse>> suspend fun updateFood(@Body remoteFood: RemoteFood): Response<NSResponse<RemoteCreateUpdateResponse>>
@GET("v3/profile?sort\$desc=date&limit=1")
suspend fun getLastProfile(): Response<NSResponse<List<JSONObject>>>
@POST("v3/profile")
suspend fun createProfile(@Body profile: JsonObject): Response<NSResponse<RemoteCreateUpdateResponse>>
} }

View file

@ -19,6 +19,7 @@ data class LastModified(
@SerializedName("entries") var entries: Long = 0, // entries collection @SerializedName("entries") var entries: Long = 0, // entries collection
@SerializedName("profile") var profile: Long = 0, // profile collection @SerializedName("profile") var profile: Long = 0, // profile collection
@SerializedName("treatments") var treatments: Long = 0, // treatments 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
) )
} }

View file

@ -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<String>,
val profiles: ArrayList<SimpleProfile>
)
@Serializable data class SimpleProfile(
@SerializedName("dia") val dia: Double,
@SerializedName("carbratio") val carbratio: ArrayList<ProfileEntry>,
@SerializedName("sens") val sens: ArrayList<ProfileEntry>,
@SerializedName("basal") val basal: ArrayList<ProfileEntry>,
@SerializedName("target_low") val target_low: ArrayList<ProfileEntry>,
@SerializedName("target_high") val target_high: ArrayList<ProfileEntry>,
@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
)
*/
}

View file

@ -22,9 +22,9 @@ import info.nightscout.interfaces.plugin.ActivePlugin
import info.nightscout.interfaces.plugin.PluginBase import info.nightscout.interfaces.plugin.PluginBase
import info.nightscout.interfaces.plugin.PluginDescription import info.nightscout.interfaces.plugin.PluginDescription
import info.nightscout.interfaces.plugin.PluginType import info.nightscout.interfaces.plugin.PluginType
import info.nightscout.interfaces.profile.Instantiator
import info.nightscout.interfaces.profile.Profile import info.nightscout.interfaces.profile.Profile
import info.nightscout.interfaces.profile.ProfileFunction import info.nightscout.interfaces.profile.ProfileFunction
import info.nightscout.interfaces.profile.Instantiator
import info.nightscout.interfaces.profile.ProfileSource import info.nightscout.interfaces.profile.ProfileSource
import info.nightscout.interfaces.profile.ProfileStore import info.nightscout.interfaces.profile.ProfileStore
import info.nightscout.interfaces.profile.PureProfile import info.nightscout.interfaces.profile.PureProfile
@ -413,6 +413,7 @@ class ProfilePlugin @Inject constructor(
} }
if (numOfProfiles > 0) json.put("defaultProfile", currentProfile()?.name) 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()) 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("startDate", dateUtil.toISOAsUTC(startDate))
json.put("store", store) json.put("store", store)
} catch (e: JSONException) { } catch (e: JSONException) {

View file

@ -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.LoadDeviceStatusWorker
import info.nightscout.plugins.sync.nsclientV3.workers.LoadFoodsWorker import info.nightscout.plugins.sync.nsclientV3.workers.LoadFoodsWorker
import info.nightscout.plugins.sync.nsclientV3.workers.LoadLastModificationWorker 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.LoadStatusWorker
import info.nightscout.plugins.sync.nsclientV3.workers.LoadTreatmentsWorker import info.nightscout.plugins.sync.nsclientV3.workers.LoadTreatmentsWorker
import info.nightscout.plugins.sync.nsclientV3.workers.ProcessFoodWorker import info.nightscout.plugins.sync.nsclientV3.workers.ProcessFoodWorker
@ -49,6 +50,7 @@ abstract class SyncModule {
@ContributesAndroidInjector abstract fun contributesLoadLastModificationWorker(): LoadLastModificationWorker @ContributesAndroidInjector abstract fun contributesLoadLastModificationWorker(): LoadLastModificationWorker
@ContributesAndroidInjector abstract fun contributesLoadBgWorker(): LoadBgWorker @ContributesAndroidInjector abstract fun contributesLoadBgWorker(): LoadBgWorker
@ContributesAndroidInjector abstract fun contributesLoadFoodsWorker(): LoadFoodsWorker @ContributesAndroidInjector abstract fun contributesLoadFoodsWorker(): LoadFoodsWorker
@ContributesAndroidInjector abstract fun contributesLoadProfileStoreWorker(): LoadProfileStoreWorker
@ContributesAndroidInjector abstract fun contributesStoreBgWorker(): StoreDataForDbImpl.StoreBgWorker @ContributesAndroidInjector abstract fun contributesStoreBgWorker(): StoreDataForDbImpl.StoreBgWorker
@ContributesAndroidInjector abstract fun contributesStoreFoodWorker(): StoreDataForDbImpl.StoreFoodWorker @ContributesAndroidInjector abstract fun contributesStoreFoodWorker(): StoreDataForDbImpl.StoreFoodWorker
@ContributesAndroidInjector abstract fun contributesTreatmentWorker(): LoadTreatmentsWorker @ContributesAndroidInjector abstract fun contributesTreatmentWorker(): LoadTreatmentsWorker

View file

@ -5,6 +5,7 @@ import info.nightscout.database.impl.AppRepository
import info.nightscout.interfaces.plugin.ActivePlugin import info.nightscout.interfaces.plugin.ActivePlugin
import info.nightscout.interfaces.profile.ProfileFunction import info.nightscout.interfaces.profile.ProfileFunction
import info.nightscout.interfaces.sync.DataSyncSelector import info.nightscout.interfaces.sync.DataSyncSelector
import info.nightscout.interfaces.utils.JsonHelper
import info.nightscout.plugins.sync.R import info.nightscout.plugins.sync.R
import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag import info.nightscout.rx.logging.LTag
@ -764,7 +765,11 @@ class DataSyncSelectorImplementation @Inject constructor(
if (lastChange == 0L) return if (lastChange == 0L) return
if (lastChange > lastSync) { if (lastChange > lastSync) {
if (activePlugin.activeProfileSource.profile?.allProfilesValid != true) return 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()), "") activePlugin.activeNsClient?.nsAdd("profile", DataSyncSelector.PairProfileStore(profileJson, dateUtil.now()), "")
} }
} }

View file

@ -16,11 +16,12 @@ import com.google.gson.GsonBuilder
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import info.nightscout.core.utils.fabric.FabricPrivacy import info.nightscout.core.utils.fabric.FabricPrivacy
import info.nightscout.core.validators.ValidatingEditTextPreference import info.nightscout.core.validators.ValidatingEditTextPreference
import info.nightscout.database.ValueWrapper
import info.nightscout.database.entities.interfaces.TraceableDBEntry import info.nightscout.database.entities.interfaces.TraceableDBEntry
import info.nightscout.database.impl.AppRepository
import info.nightscout.interfaces.Config import info.nightscout.interfaces.Config
import info.nightscout.interfaces.Constants import info.nightscout.interfaces.Constants
import info.nightscout.interfaces.nsclient.NSAlarm import info.nightscout.interfaces.nsclient.NSAlarm
import info.nightscout.interfaces.nsclient.StoreDataForDb
import info.nightscout.interfaces.plugin.PluginBase import info.nightscout.interfaces.plugin.PluginBase
import info.nightscout.interfaces.plugin.PluginDescription import info.nightscout.interfaces.plugin.PluginDescription
import info.nightscout.interfaces.plugin.PluginType import info.nightscout.interfaces.plugin.PluginType
@ -98,9 +99,9 @@ class NSClientV3Plugin @Inject constructor(
private val config: Config, private val config: Config,
private val dateUtil: DateUtil, private val dateUtil: DateUtil,
private val uiInteraction: UiInteraction, private val uiInteraction: UiInteraction,
private val storeDataForDb: StoreDataForDb,
private val dataSyncSelector: DataSyncSelector, private val dataSyncSelector: DataSyncSelector,
private val profileFunction: ProfileFunction private val profileFunction: ProfileFunction,
private val repository: AppRepository
) : NsClient, Sync, PluginBase( ) : NsClient, Sync, PluginBase(
PluginDescription() PluginDescription()
.mainType(PluginType.SYNC) .mainType(PluginType.SYNC)
@ -116,7 +117,7 @@ class NSClientV3Plugin @Inject constructor(
companion object { companion object {
val JOB_NAME: String = this::class.java.simpleName 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() private val disposable = CompositeDisposable()
@ -196,18 +197,26 @@ class NSClientV3Plugin @Inject constructor(
disposable += rxBus disposable += rxBus
.toObservable(EventNewBG::class.java) .toObservable(EventNewBG::class.java)
.observeOn(aapsSchedulers.io) .observeOn(aapsSchedulers.io)
.subscribe({ scheduleExecution("NEW_BG") }, fabricPrivacy::logException) .subscribe({ delayAndScheduleExecution("NEW_BG") }, fabricPrivacy::logException)
disposable += rxBus disposable += rxBus
.toObservable(EventNewHistoryData::class.java) .toObservable(EventNewHistoryData::class.java)
.observeOn(aapsSchedulers.io) .observeOn(aapsSchedulers.io)
.subscribe({ scheduleExecution("NEW_DATA") }, fabricPrivacy::logException) .subscribe({ delayAndScheduleExecution("NEW_DATA") }, fabricPrivacy::logException)
runLoop = Runnable { runLoop = Runnable {
handler.postDelayed(runLoop, REFRESH_INTERVAL) 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) handler.postDelayed(runLoop, REFRESH_INTERVAL)
executeLoop("START") executeLoop("START", forceNew = false)
} }
override fun onStop() { override fun onStop() {
@ -273,7 +282,7 @@ class NSClientV3Plugin @Inject constructor(
} }
override fun resend(reason: String) { override fun resend(reason: String) {
executeLoop("RESEND") executeLoop("RESEND", forceNew = false)
} }
override fun pause(newState: Boolean) { override fun pause(newState: Boolean) {
@ -331,6 +340,36 @@ class NSClientV3Plugin @Inject constructor(
enum class Operation { CREATE, UPDATE } enum class Operation { CREATE, UPDATE }
private val gson: Gson = GsonBuilder().create() 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) { private fun dbOperationDeviceStatus(collection: String = "devicestatus", dataPair: DataSyncSelector.DataPair, progress: String) {
val data = (dataPair as DataSyncSelector.PairDeviceStatus).value.toRemoteDeviceStatus() val data = (dataPair as DataSyncSelector.PairDeviceStatus).value.toRemoteDeviceStatus()
scope.launch { scope.launch {
@ -340,6 +379,7 @@ class NSClientV3Plugin @Inject constructor(
when (result.response) { when (result.response) {
200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ${dataPair.value.javaClass.simpleName}")) 200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ${dataPair.value.javaClass.simpleName}"))
201 -> rxBus.send(EventNSClientNewLog("ADDED", "OK ${dataPair.value.javaClass.simpleName} ${result.identifier}")) 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 -> { else -> {
rxBus.send(EventNSClientNewLog("ERROR", "${dataPair.value.javaClass.simpleName} ")) 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}")) 200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ${dataPair.value.javaClass.simpleName}"))
201 -> rxBus.send(EventNSClientNewLog("ADDED", "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}")) 400 -> rxBus.send(EventNSClientNewLog("FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}"))
404 -> rxBus.send(EventNSClientNewLog("NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}"))
else -> { else -> {
rxBus.send(EventNSClientNewLog("ERROR", "${dataPair.value.javaClass.simpleName} ")) 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}")) 200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ${dataPair.value.javaClass.simpleName}"))
201 -> rxBus.send(EventNSClientNewLog("ADDED", "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}")) 400 -> rxBus.send(EventNSClientNewLog("FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}"))
404 -> rxBus.send(EventNSClientNewLog("NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}"))
else -> { else -> {
rxBus.send(EventNSClientNewLog("ERROR", "${dataPair.value.javaClass.simpleName} ")) 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}")) 200 -> rxBus.send(EventNSClientNewLog("UPDATED", "OK ${dataPair.value.javaClass.simpleName}"))
201 -> rxBus.send(EventNSClientNewLog("ADDED", "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}")) 400 -> rxBus.send(EventNSClientNewLog("FAIL", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}"))
404 -> rxBus.send(EventNSClientNewLog("NOT_FOUND", "${dataPair.value.javaClass.simpleName} ${result.errorResponse}"))
else -> { else -> {
rxBus.send(EventNSClientNewLog("ERROR", "${dataPair.value.javaClass.simpleName} ")) 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) { private fun dbOperation(collection: String, dataPair: DataSyncSelector.DataPair, progress: String, operation: Operation) {
when (collection) { when (collection) {
"profile" -> dbOperationProfileStore(dataPair = dataPair, progress = progress)
"devicestatus" -> dbOperationDeviceStatus(dataPair = dataPair, progress = progress) "devicestatus" -> dbOperationDeviceStatus(dataPair = dataPair, progress = progress)
"entries" -> dbOperationEntries(dataPair = dataPair, progress = progress, operation = operation) "entries" -> dbOperationEntries(dataPair = dataPair, progress = progress, operation = operation)
"food" -> dbOperationFood(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)) 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 origin = "5_MIN_AFTER_BG"
var forceNew = true
var toTime = lastLoadedSrvModified.collections.entries + T.mins(5).plus(T.secs(10)).msecs() var toTime = lastLoadedSrvModified.collections.entries + T.mins(5).plus(T.secs(10)).msecs()
if (toTime < dateUtil.now()) { if (toTime < dateUtil.now()) {
toTime = dateUtil.now() + T.mins(1).plus(T.secs(0)).msecs() toTime = dateUtil.now() + T.mins(1).plus(T.secs(0)).msecs()
origin = "1_MIN_OLD_DATA" 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))) 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)) { if (sp.getBoolean(R.string.key_ns_client_paused, false)) {
rxBus.send(EventNSClientNewLog("RUN", "paused")) rxBus.send(EventNSClientNewLog("RUN", "paused"))
return return
@ -673,25 +719,28 @@ class NSClientV3Plugin @Inject constructor(
rxBus.send(EventNSClientNewLog("RUN", blockingReason)) rxBus.send(EventNSClientNewLog("RUN", blockingReason))
return return
} }
if (workIsRunning(arrayOf(JOB_NAME))) if (workIsRunning(arrayOf(JOB_NAME))) {
rxBus.send(EventNSClientNewLog("RUN", "Already running $origin")) rxBus.send(EventNSClientNewLog("RUN", "Already running $origin"))
else { if (!forceNew) return
rxBus.send(EventNSClientNewLog("RUN", "Starting next round $origin")) // Wait for end and start new cycle
WorkManager.getInstance(context) while (workIsRunning(arrayOf(JOB_NAME))) Thread.sleep(5000)
.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()
} }
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<String>): Boolean { private fun workIsRunning(workNames: Array<String>): Boolean {
@ -704,12 +753,12 @@ class NSClientV3Plugin @Inject constructor(
private val eventWorker = Executors.newSingleThreadScheduledExecutor() private val eventWorker = Executors.newSingleThreadScheduledExecutor()
private var scheduledEventPost: ScheduledFuture<*>? = null private var scheduledEventPost: ScheduledFuture<*>? = null
private fun scheduleExecution(origin: String) { private fun delayAndScheduleExecution(origin: String) {
class PostRunnable : Runnable { class PostRunnable : Runnable {
override fun run() { override fun run() {
scheduledEventPost = null scheduledEventPost = null
executeLoop(origin) executeLoop(origin, forceNew = true)
} }
} }
// cancel waiting task to prevent sending multiple posts // cancel waiting task to prevent sending multiple posts

View file

@ -57,7 +57,7 @@ class LoadBgWorker(
sgvs = response.values sgvs = response.values
response.lastServerModified?.let { nsClientV3Plugin.lastLoadedSrvModified.collections.entries = it } response.lastServerModified?.let { nsClientV3Plugin.lastLoadedSrvModified.collections.entries = it }
nsClientV3Plugin.storeLastLoadedSrvModified() 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") aapsLogger.debug("SGVS: $sgvs")
if (sgvs.isNotEmpty()) { if (sgvs.isNotEmpty()) {
@ -77,8 +77,7 @@ class LoadBgWorker(
nsClientV3Plugin.lastLoadedSrvModified.collections.entries = lastLoaded nsClientV3Plugin.lastLoadedSrvModified.collections.entries = lastLoaded
nsClientV3Plugin.storeLastLoadedSrvModified() nsClientV3Plugin.storeLastLoadedSrvModified()
} }
rxBus.send(EventNSClientNewLog("RCV END", "No SGVs from ${dateUtil rxBus.send(EventNSClientNewLog("RCV END", "No SGVs from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}"))
.dateAndTimeAndSecondsString(lastLoaded)}"))
WorkManager.getInstance(context) WorkManager.getInstance(context)
.beginUniqueWork( .beginUniqueWork(
NSClientV3Plugin.JOB_NAME, NSClientV3Plugin.JOB_NAME,

View file

@ -9,7 +9,6 @@ import androidx.work.workDataOf
import info.nightscout.core.utils.receivers.DataWorkerStorage import info.nightscout.core.utils.receivers.DataWorkerStorage
import info.nightscout.core.utils.worker.LoggingWorker import info.nightscout.core.utils.worker.LoggingWorker
import info.nightscout.interfaces.nsclient.StoreDataForDb import info.nightscout.interfaces.nsclient.StoreDataForDb
import info.nightscout.interfaces.workflow.WorkerClasses
import info.nightscout.plugins.sync.nsShared.StoreDataForDbImpl import info.nightscout.plugins.sync.nsShared.StoreDataForDbImpl
import info.nightscout.plugins.sync.nsclientV3.NSClientV3Plugin import info.nightscout.plugins.sync.nsclientV3.NSClientV3Plugin
import info.nightscout.rx.bus.RxBus import info.nightscout.rx.bus.RxBus
@ -30,7 +29,6 @@ class LoadFoodsWorker(
@Inject lateinit var nsClientV3Plugin: NSClientV3Plugin @Inject lateinit var nsClientV3Plugin: NSClientV3Plugin
@Inject lateinit var dateUtil: DateUtil @Inject lateinit var dateUtil: DateUtil
@Inject lateinit var storeDataForDb: StoreDataForDb @Inject lateinit var storeDataForDb: StoreDataForDb
@Inject lateinit var workerClasses: WorkerClasses
override fun doWorkAndLog(): Result { override fun doWorkAndLog(): Result {
val nsAndroidClient = nsClientV3Plugin.nsAndroidClient ?: return Result.failure(workDataOf("Error" to "AndroidClient is null")) val nsAndroidClient = nsClientV3Plugin.nsAndroidClient ?: return Result.failure(workDataOf("Error" to "AndroidClient is null"))
@ -51,7 +49,7 @@ class LoadFoodsWorker(
.setInputData(dataWorkerStorage.storeInputData(foods)) .setInputData(dataWorkerStorage.storeInputData(foods))
.build() .build()
).then(OneTimeWorkRequest.Builder(StoreDataForDbImpl.StoreFoodWorker::class.java).build()) ).then(OneTimeWorkRequest.Builder(StoreDataForDbImpl.StoreFoodWorker::class.java).build())
.then(OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build()) .then(OneTimeWorkRequest.Builder(LoadProfileStoreWorker::class.java).build())
.enqueue() .enqueue()
} else { } else {
rxBus.send(EventNSClientNewLog("RCV", "FOOD skipped")) rxBus.send(EventNSClientNewLog("RCV", "FOOD skipped"))
@ -59,7 +57,7 @@ class LoadFoodsWorker(
.enqueueUniqueWork( .enqueueUniqueWork(
NSClientV3Plugin.JOB_NAME, NSClientV3Plugin.JOB_NAME,
ExistingWorkPolicy.APPEND_OR_REPLACE, ExistingWorkPolicy.APPEND_OR_REPLACE,
OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build() OneTimeWorkRequest.Builder(LoadProfileStoreWorker::class.java).build()
) )
} }
} }

View file

@ -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<List<JSONObject>> = 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
}
}

View file

@ -69,7 +69,7 @@ internal class NSClientV3PluginTest : TestBaseWithProfile() {
sut = sut =
NSClientV3Plugin( NSClientV3Plugin(
injector, aapsLogger, aapsSchedulers, rxBus, rh, context, fabricPrivacy, 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 sut.nsAndroidClient = nsAndroidClient
`when`(mockedProfileFunction.getProfile(anyLong())).thenReturn(validProfile) `when`(mockedProfileFunction.getProfile(anyLong())).thenReturn(validProfile)
@ -519,4 +519,22 @@ internal class NSClientV3PluginTest : TestBaseWithProfile() {
verify(dataSyncSelector, Times(2)).confirmLastTherapyEventIdIfGreater(1000) verify(dataSyncSelector, Times(2)).confirmLastTherapyEventIdIfGreater(1000)
verify(dataSyncSelector, Times(2)).processChangedTherapyEvents() 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()
}
} }