NSCv3: process Food, first load based on created_at

This commit is contained in:
Milos Kozak 2022-12-28 20:17:04 +01:00
parent 10e8e32e3b
commit d9c6cd6342
25 changed files with 603 additions and 159 deletions

View file

@ -1,7 +1,6 @@
package info.nightscout.androidaps.workflow
import info.nightscout.interfaces.workflow.WorkerClasses
import info.nightscout.plugins.general.food.FoodPlugin
import info.nightscout.plugins.profile.ProfilePlugin
import info.nightscout.source.NSClientSourcePlugin
import javax.inject.Inject
@ -10,5 +9,4 @@ class WorkerClassesImpl @Inject constructor(): WorkerClasses{
override val nsClientSourceWorker = NSClientSourcePlugin.NSClientSourceWorker::class.java
override val nsProfileWorker = ProfilePlugin.NSProfileWorker::class.java
override val foodWorker = FoodPlugin.FoodWorker::class.java
}

1
connectwsa.bat Normal file
View file

@ -0,0 +1 @@
adb connect 127.0.0.1:58526

View file

@ -27,11 +27,11 @@ interface StoreDataForDb {
val temporaryBasals: MutableList<TemporaryBasal>
val profileSwitches: MutableList<ProfileSwitch>
val offlineEvents: MutableList<OfflineEvent>
val foods: MutableList<Food>
val nsIdGlucoseValues: MutableList<GlucoseValue>
val nsIdBoluses: MutableList<Bolus>
val nsIdCarbs: MutableList<Carbs>
val nsIdFoods: MutableList<Food>
val nsIdTemporaryTargets: MutableList<TemporaryTarget>
val nsIdEffectiveProfileSwitches: MutableList<EffectiveProfileSwitch>
val nsIdBolusCalculatorResults: MutableList<BolusCalculatorResult>
@ -41,8 +41,10 @@ interface StoreDataForDb {
val nsIdProfileSwitches: MutableList<ProfileSwitch>
val nsIdOfflineEvents: MutableList<OfflineEvent>
val nsIdDeviceStatuses: MutableList<DeviceStatus>
val nsIdFoods: MutableList<Food>
fun storeTreatmentsToDb()
fun storeGlucoseValuesToDb()
fun storeFoodsToDb()
fun scheduleNsIdUpdate()
}

View file

@ -16,7 +16,7 @@ interface NsClient : Sync {
fun textLog(): Spanned
fun clearLog()
enum class Collection { ENTRIES, TREATMENTS}
enum class Collection { ENTRIES, TREATMENTS, FOODS }
/**
* NSC v3 does first load of all data
* next loads are using srvModified property for sync

View file

@ -5,5 +5,4 @@ import androidx.work.ListenableWorker
interface WorkerClasses {
val nsClientSourceWorker: Class<out ListenableWorker>
val nsProfileWorker: Class<out ListenableWorker>
val foodWorker: Class<out ListenableWorker>
}

View file

@ -4,14 +4,17 @@ import android.content.Context
import info.nightscout.sdk.exceptions.DateHeaderOutOfToleranceException
import info.nightscout.sdk.exceptions.InvalidAccessTokenException
import info.nightscout.sdk.exceptions.InvalidFormatNightscoutException
import info.nightscout.sdk.exceptions.UnsuccessfullNightscoutException
import info.nightscout.sdk.exceptions.UnknownResponseNightscoutException
import info.nightscout.sdk.exceptions.UnsuccessfullNightscoutException
import info.nightscout.sdk.interfaces.NSAndroidClient
import info.nightscout.sdk.localmodel.Status
import info.nightscout.sdk.localmodel.entry.NSSgvV3
import info.nightscout.sdk.localmodel.food.NSFood
import info.nightscout.sdk.localmodel.treatment.CreateUpdateResponse
import info.nightscout.sdk.localmodel.treatment.NSTreatment
import info.nightscout.sdk.mapper.toLocal
import info.nightscout.sdk.mapper.toNSFood
import info.nightscout.sdk.mapper.toRemoteFood
import info.nightscout.sdk.mapper.toRemoteTreatment
import info.nightscout.sdk.mapper.toSgv
import info.nightscout.sdk.mapper.toTreatment
@ -19,6 +22,7 @@ import info.nightscout.sdk.networking.NetworkStackBuilder
import info.nightscout.sdk.remotemodel.LastModified
import info.nightscout.sdk.remotemodel.RemoteDeviceStatus
import info.nightscout.sdk.remotemodel.RemoteEntry
import info.nightscout.sdk.remotemodel.RemoteFood
import info.nightscout.sdk.remotemodel.RemoteTreatment
import info.nightscout.sdk.utils.retry
import info.nightscout.sdk.utils.toNotNull
@ -140,9 +144,9 @@ class NSAndroidClientImpl(
}
}
override suspend fun getTreatmentsNewerThan(from: Long, limit: Long): List<NSTreatment> = callWrapper(dispatcher) {
override suspend fun getTreatmentsNewerThan(createdAt: String, limit: Long): List<NSTreatment> = callWrapper(dispatcher) {
val response = api.getTreatmentsNewerThan(from, limit)
val response = api.getTreatmentsNewerThan(createdAt, limit)
if (response.isSuccessful) {
return@callWrapper response.body()?.result?.map(RemoteTreatment::toTreatment).toNotNull()
} else {
@ -154,7 +158,8 @@ class NSAndroidClientImpl(
val response = api.getTreatmentsModifiedSince(from, limit)
val eTagString = response.headers()["ETag"]
val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() ?: throw UnsuccessfullNightscoutException()
val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong()
?: throw UnsuccessfullNightscoutException()
if (response.isSuccessful) {
return@callWrapper NSAndroidClient.ReadResponse(eTag, response.body()?.result?.map(RemoteTreatment::toTreatment).toNotNull())
} else {
@ -207,6 +212,64 @@ class NSAndroidClientImpl(
}
}
override suspend fun getFoods(limit: Long): List<NSFood> = callWrapper(dispatcher) {
val response = api.getFoods(limit)
if (response.isSuccessful) {
return@callWrapper response.body()?.result?.map(RemoteFood::toNSFood).toNotNull()
} else {
throw UnsuccessfullNightscoutException()
}
}
/*
override suspend fun getFoodsModifiedSince(from: Long, limit: Long): NSAndroidClient.ReadResponse<List<NSFood>> = callWrapper(dispatcher) {
val response = api.getFoodsModifiedSince(from, limit)
val eTagString = response.headers()["ETag"]
val eTag = eTagString?.substring(3, eTagString.length - 1)?.toLong() ?: throw UnsuccessfullNightscoutException()
if (response.isSuccessful) {
return@callWrapper NSAndroidClient.ReadResponse(eTag, response.body()?.result?.map(RemoteFood::toNSFood).toNotNull())
} else {
throw UnsuccessfullNightscoutException()
}
}
*/
override suspend fun createFood(nsFood: NSFood): CreateUpdateResponse = callWrapper(dispatcher) {
val remoteFood = nsFood.toRemoteFood()
remoteFood.app = "AAPS"
val response = api.createFood(remoteFood)
if (response.isSuccessful) {
return@callWrapper CreateUpdateResponse(
response = response.code(),
identifier = response.body()?.result?.identifier ?: throw UnknownResponseNightscoutException(),
isDeduplication = response.body()?.result?.isDeduplication ?: false,
deduplicatedIdentifier = response.body()?.result?.deduplicatedIdentifier,
lastModified = response.body()?.result?.lastModified
)
} else {
throw UnsuccessfullNightscoutException()
}
}
override suspend fun updateFood(nsFood: NSFood): CreateUpdateResponse = callWrapper(dispatcher) {
val remoteFood = nsFood.toRemoteFood()
val response = api.updateFood(remoteFood)
if (response.isSuccessful) {
return@callWrapper CreateUpdateResponse(
response = response.code(),
identifier = response.body()?.result?.identifier ?: throw UnknownResponseNightscoutException(),
isDeduplication = response.body()?.result?.isDeduplication ?: false,
deduplicatedIdentifier = response.body()?.result?.deduplicatedIdentifier,
lastModified = response.body()?.result?.lastModified
)
} else {
throw UnsuccessfullNightscoutException()
}
}
private suspend fun <T> callWrapper(dispatcher: CoroutineDispatcher, block: suspend () -> T): T =
withContext(dispatcher) {
retry(

View file

@ -2,6 +2,7 @@ package info.nightscout.sdk.interfaces
import info.nightscout.sdk.localmodel.Status
import info.nightscout.sdk.localmodel.entry.NSSgvV3
import info.nightscout.sdk.localmodel.food.NSFood
import info.nightscout.sdk.localmodel.treatment.CreateUpdateResponse
import info.nightscout.sdk.localmodel.treatment.NSTreatment
import info.nightscout.sdk.remotemodel.LastModified
@ -23,9 +24,13 @@ interface NSAndroidClient {
suspend fun getSgvs(): List<NSSgvV3>
suspend fun getSgvsModifiedSince(from: Long, limit: Long): ReadResponse<List<NSSgvV3>>
suspend fun getSgvsNewerThan(from: Long, limit: Long): List<NSSgvV3>
suspend fun getTreatmentsNewerThan(from: Long, limit: Long): List<NSTreatment>
suspend fun getTreatmentsNewerThan(createdAt: String, limit: Long): List<NSTreatment>
suspend fun getTreatmentsModifiedSince(from: Long, limit: Long): ReadResponse<List<NSTreatment>>
suspend fun getDeviceStatusModifiedSince(from: Long): List<RemoteDeviceStatus>
suspend fun createTreatment(nsTreatment: NSTreatment): CreateUpdateResponse
suspend fun updateTreatment(nsTreatment: NSTreatment): CreateUpdateResponse
suspend fun getFoods(limit: Long): List<NSFood>
//suspend fun getFoodsModifiedSince(from: Long, limit: Long): ReadResponse<List<NSFood>>
suspend fun createFood(nsFood: NSFood): CreateUpdateResponse
suspend fun updateFood(nsFood: NSFood): CreateUpdateResponse
}

View file

@ -0,0 +1,30 @@
package info.nightscout.sdk.localmodel.food
import info.nightscout.sdk.localmodel.entry.NsUnits
data class NSFood(
val date: Long,
val device: String? = null,
val identifier: String?,
val units: NsUnits? = null,
val srvModified: Long? = null,
val srvCreated: Long? = null,
val subject: String? = null,
var isReadOnly: Boolean = false,
val isValid: Boolean,
var app: String? = null,
var name: String,
var category: String? = null,
var subCategory: String? = null,
// Example:
// name="juice" portion=250 units="ml" carbs=12
// means 250ml of juice has 12g of carbs
var portion: Double, // common portion in "units"
var carbs: Int, // in grams
var fat: Int? = null, // in grams
var protein: Int? = null, // in grams
var energy: Int? = null, // in kJ
var unit: String = "g",
var gi: Int? = null // not used yet
)

View file

@ -34,6 +34,7 @@ enum class EventType(val text: String) {
@SerializedName("Temp Basal Start") TEMPORARY_BASAL_START("Temp Basal Start"),
@SerializedName("Temp Basal End") TEMPORARY_BASAL_END("Temp Basal End"),
@SerializedName("") ERROR(""),
@SerializedName("<none>") NONE("<none>");
companion object {

View file

@ -0,0 +1,64 @@
package info.nightscout.sdk.mapper
import info.nightscout.sdk.localmodel.food.NSFood
import info.nightscout.sdk.remotemodel.RemoteFood
/**
* Convert to [RemoteFood] and back to [NSFood]
* testing purpose only
*
* @return treatment after double conversion
*/
fun NSFood.convertToRemoteAndBack(): NSFood? =
toRemoteFood().toNSFood()
internal fun RemoteFood.toNSFood(): NSFood? {
when (type) {
"food" ->
return NSFood(
date = date ?: 0L,
device = device,
identifier = identifier,
unit = unit ?: "g",
srvModified = srvModified,
srvCreated = srvCreated,
subject = subject,
isReadOnly = isReadOnly ?: false,
isValid = isValid ?: true,
name = name,
category = category,
subCategory = subcategory,
portion = portion,
carbs = carbs,
fat = fat,
protein = protein,
energy = energy,
gi = gi
)
else -> return null
}
}
internal fun NSFood.toRemoteFood(): RemoteFood =
RemoteFood(
type = "food",
date = date,
device = device,
identifier = identifier,
unit = unit,
srvModified = srvModified,
srvCreated = srvCreated,
subject = subject,
isReadOnly = isReadOnly,
isValid = isValid,
name = name,
category = category,
subcategory = subCategory,
portion = portion,
carbs = carbs,
fat = fat,
protein = protein,
energy = energy,
gi = gi
)

View file

@ -6,6 +6,7 @@ import info.nightscout.sdk.remotemodel.NSResponse
import info.nightscout.sdk.remotemodel.RemoteCreateUpdateResponse
import info.nightscout.sdk.remotemodel.RemoteDeviceStatus
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 retrofit2.Response
@ -48,7 +49,7 @@ internal interface NightscoutRemoteService {
suspend fun getSgvsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Long): Response<NSResponse<List<RemoteEntry>>>
@GET("v3/treatments")
suspend fun getTreatmentsNewerThan(@Query(value = "date\$gt", encoded = true) date: Long, @Query("limit") limit: Long): Response<NSResponse<List<RemoteTreatment>>>
suspend fun getTreatmentsNewerThan(@Query(value = "created_at\$gt", encoded = true) createdAt: String, @Query("limit") limit: Long): Response<NSResponse<List<RemoteTreatment>>>
@GET("v3/treatments/history/{from}")
suspend fun getTreatmentsModifiedSince(@Path("from") from: Long, @Query("limit") limit: Long): Response<NSResponse<List<RemoteTreatment>>>
@ -62,4 +63,16 @@ internal interface NightscoutRemoteService {
@PUT("v3/treatments")
suspend fun updateTreatment(@Body remoteTreatment: RemoteTreatment): Response<NSResponse<RemoteCreateUpdateResponse>>
@GET("v3/food")
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>>>
*/
@POST("v3/food")
suspend fun createFood(@Body remoteFood: RemoteFood): Response<NSResponse<RemoteCreateUpdateResponse>>
@PUT("v3/food")
suspend fun updateFood(@Body remoteFood: RemoteFood): Response<NSResponse<RemoteCreateUpdateResponse>>
}

View file

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

View file

@ -0,0 +1,41 @@
package info.nightscout.sdk.remotemodel
import com.google.gson.annotations.SerializedName
/**
* Depending on the type, different other fields are present.
* Those technically need to be optional.
*
* On upload a sanity check still needs to be done to verify that all mandatory fields for that type are there.
*
**/
internal data class RemoteFood(
@SerializedName("type") val type: String, // we are interesting in type "food"
@SerializedName("date") val date: Long?,
@SerializedName("name") val name: String,
@SerializedName("category") val category: String?,
@SerializedName("subcategory") val subcategory: String?,
@SerializedName("unit") val unit: String?,
@SerializedName("portion") val portion: Double,
@SerializedName("carbs") val carbs: Int,
@SerializedName("gi") val gi: Int?,
@SerializedName("energy") val energy: Int?,
@SerializedName("protein") val protein: Int?,
@SerializedName("fat") val fat: Int?,
@SerializedName("identifier")
val identifier: String?, // 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("isValid")
val isValid: Boolean?, // A flag set by the server only for deleted documents. This field appears only within history operation and for documents which were deleted by API v3 (and they always have a false value)
@SerializedName("isReadOnly")
val isReadOnly: Boolean?, // A flag set by client that locks the document from any changes. Every document marked with isReadOnly=true is forever immutable and cannot even be deleted.
@SerializedName("app") var app: String? = null, // Application or system in which the record was entered by human or device for the first time.
@SerializedName("device") val device: String? = null, // string The device from which the data originated (including serial number of the device, if it is relevant and safe).
@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("subject")
val subject: String? = null, // string Name of the security subject (within Nightscout scope) which has created the document. This field is automatically set by the server from the passed token or JWT.
@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("modifiedBy")
val modifiedBy: String? = null // string Name of the security subject (within Nightscout scope) which has patched or deleted the document for the last time. This field is automatically set by the server.
)

View file

@ -3,32 +3,33 @@ package info.nightscout.database.impl.transactions
import info.nightscout.database.entities.Food
/**
* Sync the TherapyEvents from NS
* Sync the Foods from NS
*/
class SyncNsFoodTransaction(private val food: Food) : Transaction<SyncNsFoodTransaction.TransactionResult>() {
class SyncNsFoodTransaction(private val foods: List<Food>) : Transaction<SyncNsFoodTransaction.TransactionResult>() {
override fun run(): TransactionResult {
val result = TransactionResult()
val current: Food? =
food.interfaceIDs.nightscoutId?.let {
database.foodDao.findByNSId(it)
}
for (food in foods) {
val current: Food? =
food.interfaceIDs.nightscoutId?.let {
database.foodDao.findByNSId(it)
}
if (current != null) {
// nsId exists, update if different
if (!current.contentEqualsTo(food)) {
current.copyFrom(food)
database.foodDao.updateExistingEntry(current)
if (food.isValid && current.isValid) result.updated.add(current)
else if (!food.isValid && current.isValid) result.invalidated.add(current)
if (current != null) {
// nsId exists, update if different
if (!current.contentEqualsTo(food)) {
current.copyFrom(food)
database.foodDao.updateExistingEntry(current)
if (food.isValid && current.isValid) result.updated.add(current)
else if (!food.isValid && current.isValid) result.invalidated.add(current)
}
} else {
// not known nsId, add
database.foodDao.insertNewEntry(food)
result.inserted.add(food)
}
return result
}
// not known nsId, add
database.foodDao.insertNewEntry(food)
result.inserted.add(food)
return result
}

View file

@ -3,12 +3,10 @@ package info.nightscout.plugins.di
import dagger.Module
import dagger.android.ContributesAndroidInjector
import info.nightscout.plugins.general.food.FoodFragment
import info.nightscout.plugins.general.food.FoodPlugin
@Module
@Suppress("unused")
abstract class FoodModule {
@ContributesAndroidInjector abstract fun contributesFoodFragment(): FoodFragment
@ContributesAndroidInjector abstract fun contributesFoodWorker(): FoodPlugin.FoodWorker
}

View file

@ -1,25 +1,12 @@
package info.nightscout.plugins.general.food
import android.content.Context
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import dagger.android.HasAndroidInjector
import info.nightscout.core.extensions.foodFromJson
import info.nightscout.core.utils.receivers.DataWorkerStorage
import info.nightscout.core.utils.worker.LoggingWorker
import info.nightscout.database.entities.Food
import info.nightscout.database.impl.AppRepository
import info.nightscout.database.impl.transactions.SyncNsFoodTransaction
import info.nightscout.interfaces.plugin.PluginBase
import info.nightscout.interfaces.plugin.PluginDescription
import info.nightscout.interfaces.plugin.PluginType
import info.nightscout.interfaces.utils.JsonHelper
import info.nightscout.plugins.R
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.sharedPreferences.SP
import org.json.JSONObject
import javax.inject.Inject
import javax.inject.Singleton
@ -37,73 +24,4 @@ class FoodPlugin @Inject constructor(
.shortName(R.string.food_short)
.description(R.string.description_food),
aapsLogger, rh, injector
) {
// cannot be inner class because of needed injection
class FoodWorker(
context: Context,
params: WorkerParameters
) : LoggingWorker(context, params) {
@Inject lateinit var injector: HasAndroidInjector
@Inject lateinit var repository: AppRepository
@Inject lateinit var sp: SP
@Inject lateinit var dataWorkerStorage: DataWorkerStorage
override fun doWorkAndLog(): Result {
val foods = dataWorkerStorage.pickupJSONArray(inputData.getLong(DataWorkerStorage.STORE_KEY, -1))
?: return Result.failure(workDataOf("Error" to "missing input data"))
aapsLogger.debug(LTag.DATABASE, "Received Food Data: $foods")
var ret = Result.success()
for (index in 0 until foods.length()) {
val jsonFood: JSONObject = foods.getJSONObject(index)
if (JsonHelper.safeGetString(jsonFood, "type") != "food") continue
when (JsonHelper.safeGetString(jsonFood, "action")) {
"remove" -> {
val delFood = Food(
name = "",
portion = 0.0,
carbs = 0,
isValid = false
).also { it.interfaceIDs.nightscoutId = JsonHelper.safeGetString(jsonFood, "_id") }
repository.runTransactionForResult(SyncNsFoodTransaction(delFood))
.doOnError {
aapsLogger.error(LTag.DATABASE, "Error while removing food", it)
ret = Result.failure(workDataOf("Error" to it.toString()))
}
.blockingGet()
.also {
it.invalidated.forEach { f -> aapsLogger.debug(LTag.DATABASE, "Invalidated food ${f.interfaceIDs.nightscoutId}") }
}
}
else -> {
val food = foodFromJson(jsonFood)
if (food != null) {
repository.runTransactionForResult(SyncNsFoodTransaction(food))
.doOnError {
aapsLogger.error(LTag.DATABASE, "Error while adding/updating food", it)
ret = Result.failure(workDataOf("Error" to it.toString()))
}
.blockingGet()
.also { result ->
result.inserted.forEach { aapsLogger.debug(LTag.DATABASE, "Inserted food $it") }
result.updated.forEach { aapsLogger.debug(LTag.DATABASE, "Updated food $it") }
result.invalidated.forEach { aapsLogger.debug(LTag.DATABASE, "Invalidated food $it") }
}
} else {
aapsLogger.error(LTag.DATABASE, "Error parsing food", jsonFood.toString())
ret = Result.failure(workDataOf("Error" to "Error parsing food"))
}
}
}
}
return ret
}
}
}
)

View file

@ -20,9 +20,11 @@ import info.nightscout.plugins.sync.nsclient.workers.NSClientUpdateRemoveAckWork
import info.nightscout.plugins.sync.nsclientV3.workers.DataSyncWorker
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.LoadStatusWorker
import info.nightscout.plugins.sync.nsclientV3.workers.LoadTreatmentsWorker
import info.nightscout.plugins.sync.nsclientV3.workers.ProcessFoodWorker
import info.nightscout.plugins.sync.nsclientV3.workers.ProcessTreatmentsWorker
import info.nightscout.plugins.sync.tidepool.TidepoolFragment
@ -46,11 +48,14 @@ abstract class SyncModule {
@ContributesAndroidInjector abstract fun contributesLoadStatusWorker(): LoadStatusWorker
@ContributesAndroidInjector abstract fun contributesLoadLastModificationWorker(): LoadLastModificationWorker
@ContributesAndroidInjector abstract fun contributesLoadBgWorker(): LoadBgWorker
@ContributesAndroidInjector abstract fun contributesLoadFoodsWorker(): LoadFoodsWorker
@ContributesAndroidInjector abstract fun contributesStoreBgWorker(): StoreDataForDbImpl.StoreBgWorker
@ContributesAndroidInjector abstract fun contributesStoreFoodWorker(): StoreDataForDbImpl.StoreFoodWorker
@ContributesAndroidInjector abstract fun contributesTreatmentWorker(): LoadTreatmentsWorker
@ContributesAndroidInjector abstract fun contributesProcessTreatmentsWorker(): ProcessTreatmentsWorker
@ContributesAndroidInjector abstract fun contributesLoadDeviceStatusWorker(): LoadDeviceStatusWorker
@ContributesAndroidInjector abstract fun contributesDataSyncWorker(): DataSyncWorker
@ContributesAndroidInjector abstract fun contributesFoodWorker(): ProcessFoodWorker
@ContributesAndroidInjector abstract fun contributesTidepoolFragment(): TidepoolFragment

View file

@ -26,6 +26,7 @@ import info.nightscout.database.impl.transactions.SyncNsBolusTransaction
import info.nightscout.database.impl.transactions.SyncNsCarbsTransaction
import info.nightscout.database.impl.transactions.SyncNsEffectiveProfileSwitchTransaction
import info.nightscout.database.impl.transactions.SyncNsExtendedBolusTransaction
import info.nightscout.database.impl.transactions.SyncNsFoodTransaction
import info.nightscout.database.impl.transactions.SyncNsOfflineEventTransaction
import info.nightscout.database.impl.transactions.SyncNsProfileSwitchTransaction
import info.nightscout.database.impl.transactions.SyncNsTemporaryBasalTransaction
@ -92,11 +93,11 @@ class StoreDataForDbImpl @Inject constructor(
override val temporaryBasals: MutableList<TemporaryBasal> = mutableListOf()
override val profileSwitches: MutableList<ProfileSwitch> = mutableListOf()
override val offlineEvents: MutableList<OfflineEvent> = mutableListOf()
override val foods: MutableList<Food> = mutableListOf()
override val nsIdGlucoseValues: MutableList<GlucoseValue> = mutableListOf()
override val nsIdBoluses: MutableList<Bolus> = mutableListOf()
override val nsIdCarbs: MutableList<Carbs> = mutableListOf()
override val nsIdFoods: MutableList<Food> = mutableListOf()
override val nsIdTemporaryTargets: MutableList<TemporaryTarget> = mutableListOf()
override val nsIdEffectiveProfileSwitches: MutableList<EffectiveProfileSwitch> = mutableListOf()
override val nsIdBolusCalculatorResults: MutableList<BolusCalculatorResult> = mutableListOf()
@ -106,6 +107,7 @@ class StoreDataForDbImpl @Inject constructor(
override val nsIdProfileSwitches: MutableList<ProfileSwitch> = mutableListOf()
override val nsIdOfflineEvents: MutableList<OfflineEvent> = mutableListOf()
override val nsIdDeviceStatuses: MutableList<DeviceStatus> = mutableListOf()
override val nsIdFoods: MutableList<Food> = mutableListOf()
private val userEntries: MutableList<UserEntry> = mutableListOf()
@ -131,6 +133,19 @@ class StoreDataForDbImpl @Inject constructor(
}
}
class StoreFoodWorker(
context: Context,
params: WorkerParameters
) : LoggingWorker(context, params) {
@Inject lateinit var storeDataForDb: StoreDataForDb
override fun doWorkAndLog(): Result {
storeDataForDb.storeFoodsToDb()
return Result.success()
}
}
fun <T> HashMap<T, Long>.inc(key: T) =
if (containsKey(key)) merge(key, 1, Long::plus)
else put(key, 1)
@ -171,6 +186,36 @@ class StoreDataForDbImpl @Inject constructor(
rxBus.send(EventNSClientNewLog("DONE BG", ""))
}
override fun storeFoodsToDb() {
rxBus.send(EventNSClientNewLog("PROCESSING FOOD", ""))
if (foods.isNotEmpty())
repository.runTransactionForResult(SyncNsFoodTransaction(foods))
.doOnError {
aapsLogger.error(LTag.DATABASE, "Error while saving foods", it)
}
.blockingGet()
.also { result ->
foods.clear()
result.updated.forEach {
aapsLogger.debug(LTag.DATABASE, "Updated food $it")
updated.inc(Food::class.java.simpleName)
}
result.inserted.forEach {
aapsLogger.debug(LTag.DATABASE, "Inserted food $it")
inserted.inc(Food::class.java.simpleName)
}
result.invalidated.forEach {
aapsLogger.debug(LTag.DATABASE, "Invalidated food $it")
nsIdUpdated.inc(Food::class.java.simpleName)
}
}
sendLog("Food", Food::class.java.simpleName)
SystemClock.sleep(pause)
rxBus.send(EventNSClientNewLog("DONE FOOD", ""))
}
override fun storeTreatmentsToDb() {
rxBus.send(EventNSClientNewLog("PROCESSING TR", ""))

View file

@ -41,7 +41,7 @@ import info.nightscout.plugins.sync.nsclient.data.AlarmAck
import info.nightscout.plugins.sync.nsclient.data.NSDeviceStatusHandler
import info.nightscout.plugins.sync.nsclient.workers.NSClientAddUpdateWorker
import info.nightscout.plugins.sync.nsclient.workers.NSClientMbgWorker
import info.nightscout.plugins.sync.nsclientV3.NSClientV3Plugin
import info.nightscout.plugins.sync.nsclientV3.workers.ProcessFoodWorker
import info.nightscout.rx.AapsSchedulers
import info.nightscout.rx.bus.RxBus
import info.nightscout.rx.events.EventAppExit
@ -522,11 +522,14 @@ class NSClientService : DaggerService() {
if (data.has("food")) {
val foods = data.getJSONArray("food")
if (foods.length() > 0) rxBus.send(EventNSClientNewLog("DATA", "received " + foods.length() + " foods"))
dataWorkerStorage.enqueue(
OneTimeWorkRequest.Builder(workerClasses.foodWorker)
.setInputData(dataWorkerStorage.storeInputData(foods))
.build()
)
dataWorkerStorage
.beginUniqueWork(
"ProcessFoods",
OneTimeWorkRequest.Builder(ProcessFoodWorker::class.java)
.setInputData(dataWorkerStorage.storeInputData(foods))
.build()
).then(OneTimeWorkRequest.Builder(StoreDataForDbImpl.StoreFoodWorker::class.java).build())
.enqueue()
}
if (data.has("mbgs")) {
val mbgArray = data.getJSONArray("mbgs")
@ -550,7 +553,7 @@ class NSClientService : DaggerService() {
sp.putBoolean(info.nightscout.core.utils.R.string.key_objectives_bg_is_available_in_ns, true)
dataWorkerStorage
.beginUniqueWork(
NSClientV3Plugin.JOB_NAME,
"ProcessBg",
OneTimeWorkRequest.Builder(workerClasses.nsClientSourceWorker)
.setInputData(dataWorkerStorage.storeInputData(sgvs))
.build()

View file

@ -38,6 +38,7 @@ import info.nightscout.plugins.sync.nsclientV3.extensions.toNSBolus
import info.nightscout.plugins.sync.nsclientV3.extensions.toNSBolusWizard
import info.nightscout.plugins.sync.nsclientV3.extensions.toNSCarbs
import info.nightscout.plugins.sync.nsclientV3.extensions.toNSEffectiveProfileSwitch
import info.nightscout.plugins.sync.nsclientV3.extensions.toNSFood
import info.nightscout.plugins.sync.nsclientV3.extensions.toNSProfileSwitch
import info.nightscout.plugins.sync.nsclientV3.extensions.toNSTemporaryBasal
import info.nightscout.plugins.sync.nsclientV3.extensions.toNSTemporaryTarget
@ -293,6 +294,7 @@ class NSClientV3Plugin @Inject constructor(
when (collection) {
NsClient.Collection.ENTRIES -> lastLoadedSrvModified.collections.entries == 0L
NsClient.Collection.TREATMENTS -> lastLoadedSrvModified.collections.treatments == 0L
NsClient.Collection.FOODS -> lastLoadedSrvModified.collections.foods == 0L
}
override fun updateLatestBgReceivedIfNewer(latestReceived: Long) {
@ -321,35 +323,76 @@ class NSClientV3Plugin @Inject constructor(
private val gson: Gson = GsonBuilder().create()
private fun dbOperation(collection: String, dataPair: DataSyncSelector.DataPair, progress: String, operation: Operation) {
val call = when (operation) {
Operation.CREATE -> nsAndroidClient?.let { return@let it::createTreatment }
Operation.UPDATE -> nsAndroidClient?.let { return@let it::updateTreatment }
}
when (dataPair) {
is DataSyncSelector.PairBolus -> dataPair.value.toNSBolus()
is DataSyncSelector.PairCarbs -> dataPair.value.toNSCarbs()
is DataSyncSelector.PairBolusCalculatorResult -> dataPair.value.toNSBolusWizard()
is DataSyncSelector.PairTemporaryTarget -> dataPair.value.toNSTemporaryTarget()
// is DataSyncSelector.PairFood -> dataPair.value.toJson(false)
// is DataSyncSelector.PairGlucoseValue -> dataPair.value.toJson(false, dateUtil)
is DataSyncSelector.PairTherapyEvent -> dataPair.value.toNSTherapyEvent()
is DataSyncSelector.PairTemporaryBasal -> {
val profile = profileFunction.getProfile(dataPair.value.timestamp)
if (profile == null) {
dataSyncSelector.confirmLastTemporaryBasalIdIfGreater(dataPair.id)
return
}
dataPair.value.toNSTemporaryBasal(profile)
if (collection == "food") {
val call = when (operation) {
Operation.CREATE -> nsAndroidClient?.let { return@let it::createFood }
Operation.UPDATE -> nsAndroidClient?.let { return@let it::updateFood }
}
// is DataSyncSelector.PairExtendedBolus -> dataPair.value.toJson(false, profileFunction.getProfile(dataPair.value.timestamp), dateUtil)
is DataSyncSelector.PairProfileSwitch -> dataPair.value.toNSProfileSwitch(dateUtil)
is DataSyncSelector.PairEffectiveProfileSwitch -> dataPair.value.toNSEffectiveProfileSwitch(dateUtil)
// is DataSyncSelector.PairOfflineEvent -> dataPair.value.toJson(false, dateUtil)
else -> null
}?.let { data ->
runBlocking {
if (collection == "treatments") {
when (dataPair) {
is DataSyncSelector.PairFood -> dataPair.value.toNSFood()
else -> null
}?.let { data ->
runBlocking {
try {
val id = if (dataPair.value is TraceableDBEntry) (dataPair.value as TraceableDBEntry).interfaceIDs.nightscoutId else ""
rxBus.send(
EventNSClientNewLog(
when (operation) {
Operation.CREATE -> "ADD $collection"
Operation.UPDATE -> "UPDATE $collection"
},
when (operation) {
Operation.CREATE -> "Sent ${dataPair.javaClass.simpleName} ${gson.toJson(data)} $progress"
Operation.UPDATE -> "Sent ${dataPair.javaClass.simpleName} $id ${gson.toJson(data)} $progress"
}
)
)
call?.let { it(data) }?.let { result ->
when (dataPair) {
is DataSyncSelector.PairFood -> {
if (result.response == 201) { // created
dataPair.value.interfaceIDs.nightscoutId = result.identifier
storeDataForDb.nsIdFoods.add(dataPair.value)
storeDataForDb.scheduleNsIdUpdate()
}
dataSyncSelector.confirmLastFoodIdIfGreater(dataPair.id)
}
}
}
} catch (e: Exception) {
aapsLogger.error(LTag.NSCLIENT, "Upload exception", e)
}
}
}
}
if (collection == "treatments") {
val call = when (operation) {
Operation.CREATE -> nsAndroidClient?.let { return@let it::createTreatment }
Operation.UPDATE -> nsAndroidClient?.let { return@let it::updateTreatment }
}
when (dataPair) {
is DataSyncSelector.PairBolus -> dataPair.value.toNSBolus()
is DataSyncSelector.PairCarbs -> dataPair.value.toNSCarbs()
is DataSyncSelector.PairBolusCalculatorResult -> dataPair.value.toNSBolusWizard()
is DataSyncSelector.PairTemporaryTarget -> dataPair.value.toNSTemporaryTarget()
// is DataSyncSelector.PairGlucoseValue -> dataPair.value.toJson(false, dateUtil)
is DataSyncSelector.PairTherapyEvent -> dataPair.value.toNSTherapyEvent()
is DataSyncSelector.PairTemporaryBasal -> {
val profile = profileFunction.getProfile(dataPair.value.timestamp)
if (profile == null) {
dataSyncSelector.confirmLastTemporaryBasalIdIfGreater(dataPair.id)
return
}
dataPair.value.toNSTemporaryBasal(profile)
}
// is DataSyncSelector.PairExtendedBolus -> dataPair.value.toJson(false, profileFunction.getProfile(dataPair.value.timestamp), dateUtil)
is DataSyncSelector.PairProfileSwitch -> dataPair.value.toNSProfileSwitch(dateUtil)
is DataSyncSelector.PairEffectiveProfileSwitch -> dataPair.value.toNSEffectiveProfileSwitch(dateUtil)
// is DataSyncSelector.PairOfflineEvent -> dataPair.value.toJson(false, dateUtil)
else -> null
}?.let { data ->
runBlocking {
try {
val id = if (dataPair.value is TraceableDBEntry) (dataPair.value as TraceableDBEntry).interfaceIDs.nightscoutId else ""
rxBus.send(
@ -401,7 +444,6 @@ class NSClientV3Plugin @Inject constructor(
}
dataSyncSelector.confirmLastTempTargetsIdIfGreater(dataPair.id)
}
// is DataSyncSelector.PairFood -> dataPair.value.toJson(false)
// is DataSyncSelector.PairGlucoseValue -> dataPair.value.toJson(false, dateUtil)
is DataSyncSelector.PairTherapyEvent -> {
if (result.response == 201) { // created
@ -487,6 +529,7 @@ class NSClientV3Plugin @Inject constructor(
.then(OneTimeWorkRequest.Builder(LoadBgWorker::class.java).build())
// Other Workers are enqueued after BG finish
// LoadTreatmentsWorker
// LoadFoodsWorker
// LoadDeviceStatusWorker
// DataSyncWorker
.enqueue()

View file

@ -0,0 +1,38 @@
package info.nightscout.plugins.sync.nsclientV3.extensions
import info.nightscout.database.entities.Food
import info.nightscout.database.entities.embedments.InterfaceIDs
import info.nightscout.sdk.localmodel.food.NSFood
fun NSFood.toFood(): Food =
Food(
isValid = isValid,
name = name,
category = category,
subCategory = subCategory,
portion = portion,
carbs = carbs,
fat = fat,
protein = protein,
energy = energy,
unit = unit,
gi = gi,
interfaceIDs_backing = InterfaceIDs(nightscoutId = identifier)
)
fun Food.toNSFood(): NSFood =
NSFood(
date = System.currentTimeMillis(),
isValid = isValid,
name = name,
category = category,
subCategory = subCategory,
portion = portion,
carbs = carbs,
fat = fat,
protein = protein,
energy = energy,
unit = unit,
gi = gi,
identifier = interfaceIDs.nightscoutId,
)

View file

@ -0,0 +1,68 @@
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.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
import info.nightscout.rx.events.EventNSClientNewLog
import info.nightscout.sdk.localmodel.food.NSFood
import info.nightscout.shared.utils.DateUtil
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
class LoadFoodsWorker(
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 storeDataForDb: StoreDataForDb
@Inject lateinit var workerClasses: WorkerClasses
override fun doWorkAndLog(): Result {
val nsAndroidClient = nsClientV3Plugin.nsAndroidClient ?: return Result.failure(workDataOf("Error" to "AndroidClient is null"))
// Food database doesn't provide last record modification
// Read full collection every 5th attempt
runBlocking {
if (nsClientV3Plugin.lastLoadedSrvModified.collections.foods++ % 5 == 0L) {
val foods: List<NSFood> = nsAndroidClient.getFoods(1000)
aapsLogger.debug("FOODS: $foods")
rxBus.send(EventNSClientNewLog("RCV", "${foods.size} FOODs"))
// Schedule processing of fetched data
WorkManager.getInstance(context)
.beginUniqueWork(
NSClientV3Plugin.JOB_NAME,
ExistingWorkPolicy.APPEND_OR_REPLACE,
OneTimeWorkRequest.Builder(ProcessFoodWorker::class.java)
.setInputData(dataWorkerStorage.storeInputData(foods))
.build()
).then(OneTimeWorkRequest.Builder(StoreDataForDbImpl.StoreFoodWorker::class.java).build())
.then(OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build())
.enqueue()
} else {
rxBus.send(EventNSClientNewLog("RCV", "FOOD skipped"))
WorkManager.getInstance(context)
.enqueueUniqueWork(
NSClientV3Plugin.JOB_NAME,
ExistingWorkPolicy.APPEND_OR_REPLACE,
OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build()
)
}
}
return Result.success()
}
}

View file

@ -46,7 +46,8 @@ class LoadTreatmentsWorker(
val treatments: List<NSTreatment>
val response: NSAndroidClient.ReadResponse<List<NSTreatment>>?
if (isFirstLoad) {
treatments = nsAndroidClient.getTreatmentsNewerThan(lastLoaded, 500)
val lastLoadedIso = dateUtil.toISOString(lastLoaded)
treatments = nsAndroidClient.getTreatmentsNewerThan(lastLoadedIso, 500)
response = NSAndroidClient.ReadResponse(0, treatments)
}
else {
@ -75,14 +76,13 @@ class LoadTreatmentsWorker(
nsClientV3Plugin.lastLoadedSrvModified.collections.treatments = lastLoaded
nsClientV3Plugin.storeLastFetched()
}
rxBus.send(EventNSClientNewLog("RCV END", "No TRs from ${dateUtil
.dateAndTimeAndSecondsString(lastLoaded)}"))
rxBus.send(EventNSClientNewLog("RCV END", "No TRs from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}"))
storeDataForDb.storeTreatmentsToDb()
WorkManager.getInstance(context)
.enqueueUniqueWork(
NSClientV3Plugin.JOB_NAME,
ExistingWorkPolicy.APPEND_OR_REPLACE,
OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build()
OneTimeWorkRequest.Builder(LoadFoodsWorker::class.java).build()
)
}
} catch (error: Exception) {
@ -95,14 +95,13 @@ class LoadTreatmentsWorker(
nsClientV3Plugin.lastLoadedSrvModified.collections.treatments = lastLoaded
nsClientV3Plugin.storeLastFetched()
}
rxBus.send(EventNSClientNewLog("RCV END", "No new TRs from ${dateUtil
.dateAndTimeAndSecondsString(lastLoaded)}"))
rxBus.send(EventNSClientNewLog("RCV END", "No new TRs from ${dateUtil.dateAndTimeAndSecondsString(lastLoaded)}"))
storeDataForDb.storeTreatmentsToDb()
WorkManager.getInstance(context)
.enqueueUniqueWork(
NSClientV3Plugin.JOB_NAME,
ExistingWorkPolicy.APPEND_OR_REPLACE,
OneTimeWorkRequest.Builder(LoadDeviceStatusWorker::class.java).build()
OneTimeWorkRequest.Builder(LoadFoodsWorker::class.java).build()
)
}
}

View file

@ -0,0 +1,72 @@
package info.nightscout.plugins.sync.nsclientV3.workers
import android.content.Context
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import dagger.android.HasAndroidInjector
import info.nightscout.core.extensions.foodFromJson
import info.nightscout.core.utils.receivers.DataWorkerStorage
import info.nightscout.core.utils.worker.LoggingWorker
import info.nightscout.database.entities.Food
import info.nightscout.database.impl.AppRepository
import info.nightscout.interfaces.nsclient.StoreDataForDb
import info.nightscout.interfaces.utils.JsonHelper
import info.nightscout.plugins.sync.nsclientV3.extensions.toFood
import info.nightscout.rx.logging.LTag
import info.nightscout.sdk.localmodel.food.NSFood
import info.nightscout.shared.sharedPreferences.SP
import org.json.JSONArray
import org.json.JSONObject
import javax.inject.Inject
class ProcessFoodWorker(
context: Context,
params: WorkerParameters
) : LoggingWorker(context, params) {
@Inject lateinit var injector: HasAndroidInjector
@Inject lateinit var repository: AppRepository
@Inject lateinit var sp: SP
@Inject lateinit var dataWorkerStorage: DataWorkerStorage
@Inject lateinit var storeDataForDb: StoreDataForDb
override fun doWorkAndLog(): Result {
val data = dataWorkerStorage.pickupObject(inputData.getLong(DataWorkerStorage.STORE_KEY, -1))
?: return Result.failure(workDataOf("Error" to "missing input data"))
aapsLogger.debug(LTag.DATABASE, "Received Food Data: $data")
val ret = Result.success()
val foods = mutableListOf<Food>()
if (data is JSONArray) {
for (index in 0 until data.length()) {
val jsonFood: JSONObject = data.getJSONObject(index)
if (JsonHelper.safeGetString(jsonFood, "type") != "food") continue
when (JsonHelper.safeGetString(jsonFood, "action")) {
"remove" -> {
val delFood = Food(
name = "",
portion = 0.0,
carbs = 0,
isValid = false
).also { it.interfaceIDs.nightscoutId = JsonHelper.safeGetString(jsonFood, "_id") }
foods += delFood
}
else -> {
val food = foodFromJson(jsonFood)
if (food != null) foods += food
else aapsLogger.error(LTag.DATABASE, "Error parsing food", jsonFood.toString())
}
}
}
} else if (data is List<*>) {
for (i in 0 until data.size)
foods += (data[i] as NSFood).toFood()
}
storeDataForDb.foods.addAll(foods)
return ret
}
}

View file

@ -0,0 +1,36 @@
package info.nightscout.plugins.sync.nsclientV3.extensions
import info.nightscout.database.entities.Food
import info.nightscout.database.entities.embedments.InterfaceIDs
import info.nightscout.sdk.localmodel.food.NSFood
import info.nightscout.sdk.mapper.convertToRemoteAndBack
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
@Suppress("SpellCheckingInspection")
internal class FoodExtensionKtTest {
@Test
fun toFood() {
val food = Food(
isValid = true,
name = "name",
category = "category",
subCategory = "subcategory",
portion = 2.0,
carbs = 20,
fat = 21,
protein = 22,
energy = 23,
unit = "g",
gi = 25,
interfaceIDs_backing = InterfaceIDs(
nightscoutId = "nightscoutId"
)
)
val food2 = (food.toNSFood().convertToRemoteAndBack() as NSFood).toFood()
Assertions.assertTrue(food.contentEqualsTo(food2))
Assertions.assertTrue(food.interfaceIdsEqualsTo(food2))
}
}